Runtime: HttpClient генерирует исключение TaskCanceledException по тайм-ауту

Созданный на 25 мая 2017  ·  119Комментарии  ·  Источник: dotnet/runtime

В некоторых случаях HttpClient генерирует исключение TaskCanceledException по тайм-ауту. Это происходит у нас, когда сервер находится под большой нагрузкой. Мы смогли обойти это, увеличив время ожидания по умолчанию. В следующей ветке форума MSDN отражена суть возникшей проблемы: https://social.msdn.microsoft.com/Forums/en-US/d8d87789-0ac9-4294-84a0-91c9fa27e353/bug-in-httpclientgetasync-should-throw .

Спасибо

area-System.Net.Http enhancement

Самый полезный комментарий

@karelz добавляет примечание в качестве еще одного клиента, затронутого этим вводящим в заблуждение исключением :)

Все 119 Комментарий

@awithy какую версию .NET Core вы используете?

1.1

Независимо от того, правильный дизайн или нет, по дизайну OperationCanceledExceptions выбрасываются для тайм-аутов (и TaskCanceledException является OperationCanceledException).
копия: @davidsh

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

Еще раз спасибо за быстрый ответ.

Похоже на дизайн + за последние 6+ месяцев было всего 2 жалобы клиентов.
Закрытие.

@karelz добавляет примечание в качестве еще одного клиента, затронутого этим вводящим в заблуждение исключением :)

Спасибо за информацию, пожалуйста, проголосуйте также за топовый пост (вопросы можно сортировать по нему), спасибо!

Независимо от того, правильный дизайн или нет, по дизайну OperationCanceledExceptions выбрасываются для тайм-аутов.

Это плохой дизайн имхо. Невозможно определить, действительно ли запрос был отменен (т. е. токен отмены, переданный SendAsync , был отменен) или истекло время ожидания. В последнем случае имеет смысл повторить попытку, а в первом нет. Для этого случая идеально подходит TimeoutException , почему бы не использовать его?

Да, это также поставило нашу команду в тупик. Существует целый поток переполнения стека, посвященный попыткам справиться с этим :

Несколько дней назад я опубликовал обходной путь: https://www.thomaslevesque.com/2018/02/25/better-timeout-handling-with-httpclient/

Потрясающе, спасибо.

Повторное открытие, так как интерес теперь больше - поток StackOverflow теперь имеет 69 голосов.

копия @dotnet/ncl

Какие-нибудь Новости ? Все еще происходит в dotnet core 2.0

Это в нашем списке главных проблем для пост-2.1. Однако это не будет исправлено в 2.0/2.1.

У нас есть несколько вариантов, что делать, когда происходит тайм-аут:

  1. Бросьте TimeoutException вместо TaskCanceledException .

    • Pro: легко отличить тайм-аут от явного действия отмены во время выполнения.

    • Минусы: техническое критическое изменение — генерируется новый тип исключения.

  2. Бросьте TaskCanceledException с внутренним исключением как TimeoutException

    • Pro: можно отличить тайм-аут от явного действия отмены во время выполнения. Совместимый тип исключения.

    • Открытый вопрос: можно ли выбросить исходный стек TaskCanceledException и создать новый, сохранив при этом InnerException (как TimeoutException.InnerException )? Или мы должны сохранить и просто обернуть исходный TaskCanceledException ?



      • Либо: новое исключение TaskCanceledException -> новое исключение TimeoutException -> исходное исключение TaskCanceledException (с исходным InnerException, которое может быть нулевым)


      • Или: новое TaskCanceledException -> новое TimeoutException -> исходное TaskCanceledException.InnerException (может быть нулевым)



  3. Бросьте TaskCanceledException с сообщением о тайм-ауте в качестве причины

    • Pro: можно отличить тайм-аут от явного действия отмены из сообщения/журналов.

    • Минусы: нельзя отличить во время выполнения, это «только отладка».

    • Открытый вопрос: То же, что и в [2] - должны ли мы обернуть или заменить исходное исключение TaskCanceledException (и оно складывается)

Я склоняюсь к варианту [2], с отбрасыванием исходного стека исходных TaskCanceledException .
@stephentoub @davidsh есть мысли?

Кстати: изменение должно быть довольно простым в HttpClient.SendAsync , где мы устанавливаем тайм-аут:
https://github.com/dotnet/corefx/blob/30fb78875148665b98748ede3013641734d9bf5c/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L433 -L461

Исключением верхнего уровня всегда должно быть HttpRequestException. Это стандартная модель API для HttpClient.

В рамках этого, как внутреннее исключение, «тайм-ауты», вероятно, должны быть TimeoutException. На самом деле, если бы мы объединили это с другими проблемами (об изменении HttpRequestion для включения нового свойства перечисления, подобного WebExceptionStatus), то разработчикам нужно было бы проверять только это перечисление и вообще не проверять внутренние исключения.

В основном это вариант 1), но по-прежнему сохраняется текущая модель API-совместимости с приложениями, согласно которой исключения верхнего уровня из API-интерфейсов HttpClient всегда являются HttpRequestException.

Примечание: любые «тайм-ауты» от прямого чтения потока ответов HTTP из Stream API, например:

```С#
клиент var = новый HttpClient();
Поток responseStream = await client.GetStreamAsync (url);

int bytesRead = await responseStream.ReadAsync (буфер, 0, буфер. Длина);
```

должен продолжать генерировать System.IO.IOException и т. д. как исключения верхнего уровня (на самом деле у нас есть несколько ошибок в SocketsHttpHandler по этому поводу).

@davidsh вы предлагаете бросать HttpRequestException даже в тех случаях, когда есть явная отмена пользователем? Те бросают TaskCanceledException сегодня, и вроде нормально. Не уверен, что мы хотим что-то изменить...
Я был бы в порядке, если бы тайм-ауты на стороне клиента отображались как HttpRequestException вместо TaskCanceledException .

@davidsh вы предлагаете генерировать HttpRequestException даже в тех случаях, когда есть явная отмена пользователя? Сегодня они бросают TaskCanceledException, и все в порядке. Не уверен, что мы хотим что-то изменить…

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

Кроме того, в некоторых случаях некоторые из наших стеков генерируют исключение HttpRequestException с внутренним исключением OperationCanceledException. TaskCanceledException является производным типом OperationCanceledException.

при явной отмене пользователем

Нам также необходимо четкое определение того, что такое «явное». Например, является ли установка свойства HttpClient.Timeout «явной»? Я думаю, мы говорим нет. Как насчет вызова .Cancel() для объекта CancellationTokenSource (который влияет на переданный объект CancellationToken)? Это "явно"? Вероятно, есть и другие примеры, о которых мы можем подумать.

.NET Framework также выдает TaskCanceledException, когда вы устанавливаете HttpClient.Timeout .

Если вы не можете отличить отмену от клиента и отмену от реализации - просто отметьте обе. Перед использованием клиентского токена создайте производный:
```С#
вар clientToken = ....
var innerTokenSource = CancellationTokenSource.CreateLinkedTokenSource (clientToken);

If at some point you catch **OperationCancelledException**:
```c#
try
{
     //invocation with innerToken
}
catch(OperationCancelledException oce)
{
      if(clientToken.IsCancellationRequested)
          throw; //as is, it is client initiated and in higher priority
      if(innerToken.IsCancellationRequested)
           //wrap it in HttpException, TimeoutException, whatever, wrap it in another linked token until you process all cases.
}

По сути, вы фильтруете все возможные слои от токена клиента до самого глубокого, пока не найдете токен, который вызвал отмену.

Есть какие-нибудь обновления по поводу этой версии 2.2 или 3.0? @karelz @davidsh , копия @NickCraver . Я бы хотел, чтобы он просто сделал вариант 1) для создания исключения TimeoutException или, альтернативно, нового исключения HttpTimeoutException, полученного из исключения HttpRequestException. Спасибо!

Есть какие-нибудь обновления по поводу этой версии 2.2 или 3.0?

Слишком поздно для версии 2.2, она вышла на прошлой неделе ;)

Слишком поздно для версии 2.2, она вышла на прошлой неделе ;)

Арх, вот что происходит, когда я отдыхаю ;)

Хотя это немного глупо, но самый простой обходной путь, который я нашел, — это вложить обработчик catch для OperationCanceledException, а затем оценить значение IsCancellationRequested, чтобы определить, был ли токен отмены результатом тайм-аута или фактического прерывания сеанса. Очевидно, что логика, выполняющая вызов API, скорее всего, будет на уровне бизнес-логики или сервиса, но для простоты я объединил пример:

````csharp
[HttpGet]
общедоступная асинхронная задачаИндекс (CancellationToken CancellationToken)
{
пытаться
{
//Обычно это делается в бизнес-логике или сервисном уровне, а не в самом контроллере, но я проиллюстрирую это здесь для простоты
пытаться
{
//Использование HttpClientFactory для экземпляров HttpClient
вар httpClient = _httpClientFactory.CreateClient();

        //Set an absurd timeout to illustrate the point
        httpClient.Timeout = new TimeSpan(0, 0, 0, 0, 1);

        //Perform call that requires special timeout logic                
        var httpResponse = await httpClient.GetAsync("https://someurl.com/api/long/running");

        //... (if GetAsync doesn't fail, handle the response as desired)
    }
    catch (OperationCanceledException innerOperationCanceled)
    {
        //If a canceled token exception occurs due to a timeout, "IsCancellationRequested" should be false
        if (cancellationToken.IsCancellationRequested)
        {
            //Bubble exception to global handler
            throw innerOperationCanceled;
        }
        else
        {
            //... (perform timeout logic here)
        }                    
    }                

}
catch (OperationCanceledException operationCanceledEx)
{
    _logger.LogWarning(operationCanceledEx, "Request was aborted by the end user.");
    return new StatusCodeResult(499);
}
catch (Exception ex)
{
    _logger.LogError(ex, "An unexepcted error occured.");
    return new StatusCodeResult(500);
}

}
````

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

Работа с http-клиентом совершенно ужасна. HttpClient необходимо удалить и начать с нуля.

Хотя это немного глупо, но самый простой обходной путь, который я нашел, — это вложить обработчик catch для OperationCanceledException, а затем оценить значение IsCancellationRequested, чтобы определить, был ли токен отмены результатом тайм-аута или фактического прерывания сеанса. Очевидно, что логика, выполняющая вызов API, скорее всего, будет на уровне бизнес-логики или сервиса, но для простоты я объединил пример:

[HttpGet]
public async Task<IActionResult> Index(CancellationToken cancellationToken)
{
  try
  {
      //This would normally be done in a business logic or service layer rather than in the controller itself but I'm illustrating the point here for simplicity sake
      try
      {
          //Using HttpClientFactory for HttpClient instances      
          var httpClient = _httpClientFactory.CreateClient();

          //Set an absurd timeout to illustrate the point
          httpClient.Timeout = new TimeSpan(0, 0, 0, 0, 1);

          //Perform call that requires special timeout logic                
          var httpResponse = await httpClient.GetAsync("https://someurl.com/api/long/running");

          //... (if GetAsync doesn't fail, handle the response as desired)
      }
      catch (OperationCanceledException innerOperationCanceled)
      {
          //If a canceled token exception occurs due to a timeout, "IsCancellationRequested" should be false
          if (cancellationToken.IsCancellationRequested)
          {
              //Bubble exception to global handler
              throw innerOperationCanceled;
          }
          else
          {
              //... (perform timeout logic here)
          }                    
      }                

  }
  catch (OperationCanceledException operationCanceledEx)
  {
      _logger.LogWarning(operationCanceledEx, "Request was aborted by the end user.");
      return new StatusCodeResult(499);
  }
  catch (Exception ex)
  {
      _logger.LogError(ex, "An unexepcted error occured.");
      return new StatusCodeResult(500);
  }
}

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

Это абсолютно безумие, нам нужно писать этот ужасный код. HttpClient — самая дырявая абстракция, когда-либо созданная Microsoft

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

Работа с http-клиентом совершенно ужасна. HttpClient необходимо удалить и начать с нуля.\

Это не очень полезный или конструктивный отзыв @dotnetchris . Клиенты и сотрудники MSFT все здесь, чтобы попытаться улучшить ситуацию, и поэтому такие люди, как @karelz , все еще здесь, прислушиваются к отзывам клиентов и пытаются совершенствоваться. От них не требуется заниматься разработкой в ​​открытую, и давайте не будем сжигать их добрую волю негативом, иначе они могут решить забрать свой мяч и уйти домой, и это будет хуже для сообщества. Спасибо! :)

Почему бы просто не создать HttpClient2 (временное имя) с ожидаемым поведением и сообщить в документации, что «HttpClient устарел, используйте HttpClient2» и объяснить разницу между ними.

Таким образом, в .NetFramework не вносятся критические изменения.

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

Я даже написал в блоге о том, как плохо использовать HttpClient более 3 лет назад. https://dotnetchris.wordpress.com/2015/12/11/working-with-httpclient-is-absurd/

Это явно плохой дизайн.

Кто-нибудь в Microsoft даже использовал HttpClient? Это точно не похоже.

Затем, помимо этой ужасной поверхности API, есть всевозможные дырявые абстракции, касающиеся его построения и многопоточности. Я не могу поверить, что это можно спасти, и только совершенно новый является жизнеспособным решением. HttpClient2, HttpClientSlim в порядке или что-то в этом роде.

Доступ по протоколу Http никогда не должен вызывать исключений. Он всегда должен возвращать результат, результат должен быть проверен на предмет завершения, тайм-аута, http-ответа. Единственная причина, по которой ему должно быть позволено генерировать исключение, заключается в том, что если вы вызываете EnsureSuccess() , тогда можно генерировать любое исключение в мире. Но для совершенно нормального поведения в Интернете не должно быть никакой обработки потока управления: хост работает, хост не работает, тайм-аут хоста. Ни один из них не является исключительным.

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

@dotnetchris, поскольку похоже, что вы очень увлечены этой темой, почему бы не потратить время на то, чтобы написать предлагаемый вами полный API для HttpClient2 и представить его команде для обсуждения в качестве новой проблемы?

Я не стал углубляться в суть того, что делает HttpClient хорошим или плохим на более глубоком техническом уровне, поэтому мне нечего добавить к комментариям @dotnetchris. Я определенно не думаю, что уместно критиковать команду разработчиков MS, но я также понимаю, как неприятно тратить часы и часы на поиск решений в ситуациях, которые, казалось бы, должны быть очень простыми. Это разочарование от попыток понять, почему были приняты определенные решения/изменения, в надежде, что кто-то в MS решит проблему, в то же время все еще пытаясь выполнить вашу работу и выполнить ее.

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

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

@karelz , что потребуется, чтобы получить это по графику для 3.0, предложение API от кого-то?

Речь идет не о предложении API. Это поиск правильной реализации. Он находится в нашем списке незавершенных проектов версии 3.0 и занимает довольно высокое место в списке (после HTTP/2 и корпоративных сценариев). Это очень хороший шанс быть реализованным в версии 3.0 нашей командой.
Конечно, если кто-то в сообществе мотивирован, мы тоже примем участие.

@karelz , как сообщество может помочь в этом случае? Причина, по которой я упомянул о необходимости предложения API, заключается в том, что я не был уверен в вашем сообщении, начинающемся со слов «У нас есть несколько вариантов, что делать, когда истекает время ожидания:», если команда выбрала предпочтительный вариант из тех, которые вы перечислили. Если вы определились с направлением, то мы можем пойти оттуда. Спасибо!

С технической точки зрения это не вопрос предложения API. Решение не потребует одобрения API. Тем не менее, я согласен с тем, что мы должны договориться о том, как выявить разницу - вероятно, будет какое-то обсуждение.
Я не ожидаю, что варианты слишком сильно отличаются друг от друга в реализации — если сообщество хочет помочь, реализация любого из вариантов даст нам 98% там (при условии, что исключение генерируется из одного места и может быть легко изменить позже, чтобы адаптироваться к параметрам [1]-[3], перечисленным выше).

Привет! Мы тоже недавно коснулись этого вопроса. Просто любопытно, он все еще находится на вашем радаре для 3.0?

Да, он все еще находится в нашем списке 3.0 — я бы все равно дал ему 90% шансов, что он будет исправлен в 3.0.

Я наткнулся на это сегодня. Я установил тайм-аут на HttpClient, и это дает мне это

{System.Threading.Tasks.TaskCanceledException: A task was canceled.}
CancellationRequested = true

Я согласен с другими, что это сбивает с толку, поскольку в нем не упоминается тайм-аут.

Эм, хорошо, я немного запутался. Кажется, все в этой теме говорят, что тайм-ауты HTTP вызывают TaskCanceledException , но это не то, что я получаю... Я получаю OperationCanceledException в .NET Core 2.1...

Если вы хотите сгенерировать OperationCanceledException для тайм-аутов, тогда я могу обойти это, но я просто потратил часы, пытаясь отследить, что, черт возьми, происходит, потому что это не задокументировано должным образом:

https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.sendasync

В нем четко указано, что для тайм-аутов выдается HttpRequestException ...

TaskCanceledException является производным от OperationCanceledException.

@stephentoub Да, но я не получаю TaskCanceledException , я получаю OperationCanceledException .

Например, блок catch, который перехватывает TaskCanceledException , не перехватывает исключение, ему нужен блок catch для OperationCanceledException .

Сейчас я специально тестирую тайм-ауты HTTP, фильтруя тип исключения, и я знаю, что это тайм-аут, если это НЕ TaskCanceledException :

```С#
пытаться {
используя (var response = await s_httpClient.SendAsync (запрос, _updateCancellationSource.Token).ConfigureAwait (false))
return (int)response.StatusCode < 400;
}
catch (OperationCanceledException ex) когда (ex.GetType() != typeof(TaskCanceledException)) {
// Тайм-аут HTTP
вернуть ложь;
}
поймать (HttpRequestException) {
вернуть ложь;
}

or alternatively this works as well, which might be better:

```c#
            try {
                using (var response = await s_httpClient.SendAsync(request, _updateCancellationSource.Token).ConfigureAwait(false))
                    return (int)response.StatusCode < 400;
            }
            catch (OperationCanceledException ex) when (ex.GetType() == typeof(OperationCanceledException)) {
                // HTTP timeout
                return false;
            }
            catch (HttpRequestException) {
                return false;
            }

Эм, хорошо, я немного запутался. Кажется, все в этой теме говорят, что тайм-ауты HTTP вызывают TaskCanceledException , но это не то, что я получаю... Я получаю OperationCanceledException в .NET Core 2.1...

Если вы хотите сгенерировать OperationCanceledException для тайм-аутов, тогда я могу обойти это, но я просто потратил часы, пытаясь отследить, что, черт возьми, происходит, потому что это не задокументировано должным образом:

https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.sendasync

В нем четко указано, что для тайм-аутов выдается HttpRequestException ...

Вы прочитали всю ветку, потому что на самом деле есть несколько ответов, в которых упоминается OperationCanceledExecption, включая приведенный мной пример кода, который показывает способ (не такой чистый, как мне хотелось бы, но это способ) для явной обработки тайм-аутов.

@Hobray Да, у меня есть - почему ты думаешь, что я этого не делал? Это по-прежнему не объясняет, сколько людей в этой ветке предположительно получают TaskCanceledException по тайм-аутам, а я нет.

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

@mikernet Может быть, формулировка сбила меня с толку. Вы правы, что я не учел возможные условия гонки. Я понимаю, что это не отличное решение, но если вы хотите объяснить, что я упустил из виду, я хотел бы это услышать.

Одна вещь, которую я заметил, заключается в том, что точное исключение, которое вы получаете, похоже, зависит от того, происходит ли тайм-аут или отмена при вызове контроллера и промежуточного программного обеспечения. Внутри вызова промежуточного программного обеспечения кажется, что это просто TaskCanceledException . К сожалению, у меня нет времени копаться в библиотеках кода .NET Core, чтобы понять почему, но я это заметил.

@Hobray Ха-ха, да, конечно - извините, я не хотел быть загадочным в отношении условий гонки, я просто разговаривал по телефону, пока был в пути, печатая последнее сообщение, что затрудняло объяснение.

Состояние гонки в вашем решении возникает, если отмена задачи инициируется между временем ожидания HttpClient и моментом, когда вы проверяете состояние отмены токена отмены. В моей ситуации мне нужно, чтобы исключение отмены задачи продолжало всплывать, чтобы исходящий код мог обнаружить отмену пользователя, поэтому мне нужно иметь возможность перехватывать исключение, только если оно действительно истекло, поэтому я использую when фильтровать улов. Использование вашего решения по проверке токена может привести к тому, что необработанный OperationCanceledException пройдет фильтр, а программа всплывет и сломается, потому что кто-то, отменяющий операцию вверх по течению, будет ожидать, что будет выброшено TaskCanceledException , а не OperationCanceledException .

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

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

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

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

Мне все еще неясно, каким должен быть правильный обходной путь.

Мое решение является наиболее правильным в ситуациях, когда тайм-ауты HTTP вызывают исключение OperationCanceledException. Я не могу воспроизвести ситуацию, когда выдается TaskCanceledException, и никто не предоставил способ воспроизвести такое поведение, поэтому я предлагаю протестировать свою среду, и если она соответствует моей, то это правильное решение.

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

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

@mikernet , выбрасывается ли TaskCanceledException или OperationCanceledException , в значительной степени не имеет значения. Это деталь реализации, на которую вы не должны полагаться. Что вы можете сделать, так это поймать OperationException (который также поймает TaskCanceledException ) и проверить, отменен ли переданный вами токен отмены, как описано в комментарии Хобрея (https://github.com/ dotnet/corefx/issues/20296#issuecomment-449510983).

@thomaslevesque Я уже говорил, почему этот подход является проблемой. Без рассмотрения этого ваш комментарий не очень полезен - это просто перефразирование утверждений до того, как я присоединился к преобразованию, не решая описанную мной проблему.

Что касается "это деталь реализации" - да, но, к сожалению, мне приходится полагаться на нее, потому что в противном случае нет хорошего способа определить, действительно ли задача была отменена или истекло время ожидания. Должен быть 100% способ определить из исключения, какой из этих двух сценариев произошел, чтобы избежать описанного состояния гонки. OperationCanceledException просто плохой выбор из-за этого... в фреймворке уже есть TimeoutException , который делает его лучшим кандидатом.

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

Кроме того, с точки зрения того, что тип исключения является деталью реализации... Я действительно не думаю, что это верно для исключений, по крайней мере, не с точки зрения хорошо зарекомендовавших себя практик документации .NET. Назовите еще один пример в .NET Framework, где в документации говорится, что «выдает исключение X, когда происходит Y», а инфраструктура фактически выдает общедоступный подкласс этого исключения, не говоря уже об общедоступном подклассе, который используется для совершенно другого случая исключения на тот же метод ! Это просто кажется дурным тоном, и я никогда не сталкивался с этим за 15 лет программирования на .NET.

В любом случае, TaskCanceledException следует зарезервировать для отмены задачи, инициированной пользователем, а не для тайм-аутов — как я уже сказал, у нас уже есть исключение для этого. Библиотеки должны фиксировать любые внутренние отмены задач и создавать более подходящие исключения. Обработчики исключений для отмены задач, которые были инициированы с помощью токенов отмены задач, не должны нести ответственность за решение других несвязанных проблем, которые могут возникнуть во время асинхронной операции.

@mikernet извините, я пропустил эту часть обсуждения. Да, есть состояние гонки, о котором я не подумал. Но это может быть не такой большой проблемой, как кажется... Состояние гонки возникает, если токен отмены сигнализируется после истечения времени ожидания, но до того, как вы проверите состояние токена, верно? В этом случае на самом деле не имеет значения, произошел ли тайм-аут, если операция все равно отменяется. Таким образом, вы позволите всплыть OperationCanceledException , позволив вызывающему абоненту поверить, что операция была успешно отменена, что является ложью, поскольку на самом деле произошел тайм-аут, но это не имеет значения для вызывающего абонента, поскольку он хотел в любом случае прервите операцию.

Должен быть 100% способ определить из исключения, какой из этих двух сценариев произошел, чтобы избежать описанного состояния гонки. OperationCanceledException просто плохой выбор из-за этого... в фреймворке уже есть TimeoutException , который делает его лучшим кандидатом.

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

@thomaslevesque Ну да, но я как бы обратился к этому. Это может быть или не быть важным для некоторых приложений. Хотя в моей так. Пользователь моего библиотечного метода не ожидает OperationCanceledException при отмене задачи — он ожидает TaskCanceledException , и он должен быть в состоянии поймать это и быть «в безопасности». Если я проверю токен отмены, увижу, что он был отменен, и передам исключение, то я только что передал исключение, которое не будет перехвачено их обработчиком и приведет к сбою программы. Мой метод не должен вызывать тайм-ауты.

Конечно, есть способы обойти это ... Я могу просто создать новую обернутую TaskCanceledException , но тогда я потеряю трассировку стека верхнего уровня, а тайм-ауты HTTP обрабатываются совсем иначе, чем отмены задач в моем сервисе - тайм-ауты HTTP идут в кеш в течение длительного времени, тогда как отмена задачи не будет. Я не хочу пропустить кеширование фактических тайм-аутов из-за состояния гонки.

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

В любом случае это хакерство и, как вы указали, должно быть как-то решено.

(Состояние гонки будет возникать только в средах, где TaskCanceledException генерируется вместо OperationCanceledException , где бы это ни было, но, по крайней мере, решение защищено от всплытия неожиданного типа исключения и не полагаться на детали реализации, чтобы по-прежнему работать достаточно хорошо, если детали реализации изменятся).

Исключение тайм-аута, создаваемое HttpClient в случае тайм-аута http, не должно быть типа System.Net.HttpRequestException (или потомка?) вместо общего System.TimeoutException ? Например, старый добрый WebClient выбрасывает WebException со свойством Status , для которого можно установить значение WebExceptionStatus.Timeout .

К сожалению, мы не сможем закончить это для версии 3.0. Движение в Будущее.
Это все еще на вершине нашего списка невыполненных работ, но реально мы не сможем сделать это для версии 3.0 :(.

@karelz, о нет :( Я просто собирался сделать репост, рассказав, как я снова столкнулся с этим во второй компании подряд. Как долго будет окно, чтобы отправить PR для 3.0, если я хочу заняться этим самостоятельно?

Мне трудно отследить, что именно происходит в куче комментариев выше, когда люди сообщают о различиях между получением TaskCancelledException и OperationCancelledException в зависимости от контекста, может ли кто-нибудь из MSFT осветить это?

Нравится этот комментарий от @mikernet :

Например, блок catch, который перехватывает TaskCanceledException, не перехватывает исключение, ему специально нужен блок catch для OperationCanceledException.

копия @davidsh @stephentoub может быть?

TaskCanceledException является производным от OperationCanceledException и содержит дополнительную информацию. Просто поймайте OperationCanceledException.

@karelz , будет ли вариант 1, который вы опубликовали изначально, просто выбрасывая исключение Timeout, совершенно неприятным?

Такие вещи, как StackExchange.Redis, выдают исключение, полученное из исключения Timeout, поэтому было бы неплохо просто поймать исключение Timeout для HttpClient...

Я понимаю, что @stephentoub получил наследство, но только этот комментарий ниже от @mikernet поражает меня ... Это поведение, которое он описывает, должно быть какой-то ошибкой ?? Но если это правда, это повлияет на исправление или обходной путь…

«блок перехвата, который перехватывает TaskCanceledException, не перехватывает исключение, ему специально нужен блок перехвата для OperationCanceledException»

Или вы говорите, что в некоторых местах он выдает OperationEx, а в других местах TaskEx? Например, может быть, люди видят что-то, что исходит от промежуточного программного обеспечения ASP, и думают, что это от HttpClient?

просто этот комментарий ниже от @mikernet поражает меня ... Это поведение, которое он описывает, должно быть какой-то ошибкой ??

Почему это "сводит с ума"? Если исключение B происходит от исключения A, блок перехвата для B не будет перехватывать исключение, которое конкретно является A, например, если у меня есть блок перехвата для ArgumentNullException, он не будет перехватывать " throw new ArgumentException(...)".

Или вы говорите, что в некоторых местах он выдает OperationEx, а в других местах TaskEx?

Правильно. В целом код должен предполагать только OCE; в соответствии с тем, как все написано, некоторый код может в конечном итоге выдать производный TCE, например, задача, выполненная с помощью TaskCompletionSource.TrySetCanceled, вызовет исключение TaskCanceledException.

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

@stephentoub , это руководство где-нибудь задокументировано? Пингинг @guardrex

В общем случае код должен предполагать только OCE...

Сейчас я работаю с набором документов ASP.NET. Откройте вопрос для кошек dotnet/docs...

https://github.com/dotnet/docs/issues

Можно ли возобновить этот разговор еще раз, когда были какие-то конкретные предложения с @karelz и @davidsh ? Просто кажется, что кто-то, кто владеет моделью HttpClient API, должен принимать решения о том, в каком направлении двигаться. Было некоторое обсуждение того, что исключения верхнего уровня всегда являются HttpRequestException (кроме явных отмен?), смешанные с какой-то более долгосрочной темой о более крупном рефакторинге для изменения HttpRequestion, чтобы включить новое свойство перечисления, подобное WebExceptionStatus...

Просто кажется, что вся эта проблема увязывается с необходимостью фактического изменения кода. Есть ли другие люди, которые имеют долю/право собственности на модель API HttpClient, которую следует использовать для принятия некоторых решений? Огромное спасибо всем!

У меня та же проблема, однако cancellationToken.IsCancellationRequested возвращает true :

    public class TimeoutHandler : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            try
            {
                var response = await base.SendAsync(request, cancellationToken);
                response.EnsureSuccessStatusCode();

                return response;
            }
            catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
            {
                throw new TimeoutException("Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding.");
            }
        }

Я форсирую тайм-аут. Вызвано исключение TaskCancelledException с IOException в качестве внутреннего исключения:

System.Threading.Tasks.TaskCanceledException: The operation was canceled. ---> System.IO.IOException: Unable to read data from the transport connection: A operação de E/S foi anulada devido a uma saída de thread ou a uma requisição de aplicativo. ---> System.Net.Sockets.SocketException: A operação de E/S foi anulada devido a uma saída de thread ou a uma requisição de aplicativo
   --- End of inner exception stack trace ---
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error)
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.GetResult(Int16 token)
   at System.Net.Http.HttpConnection.FillAsync()
   at System.Net.Http.HttpConnection.ReadNextResponseHeaderLineAsync(Boolean foldedHeadersAllowed)
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithNtConnectionAuthAsync(HttpConnection connection, HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

...

(Извините за переведенный текст исключения — очевидно, кто-то подумал, что было бы неплохо перенести это и в .NET Core)

Как видите, я надеялся проверить IsCancellationRequest , чтобы обойти эту проблему, как предлагали другие, но даже по истечении времени ожидания он возвращает true .

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

Привет @FelipeTaveira ,

Ваш подход не работает, потому что отмена обрабатывается выше по течению от вашего TimeoutHandler , самим HttpClient . Ваш обработчик получает токен отмены, который отменяется HttpClient , когда происходит Timeout . Таким образом, проверка того, что токен отмены не отменен, работает только в коде, вызывающем HttpClient , а не в коде , вызываемом HttpClient .

Способ заставить его работать с пользовательским обработчиком — отключить HttpClient Timeout , установив для него значение Timeout.InfiniteTimeSpan и контролируя поведение тайм-аута в вашем собственном обработчике, как показано в этой статье (полный код здесь )

@thomaslevesque О, спасибо! Я прочитал ваш код и подумал, что эта часть касается поддержки конфигурации тайм-аута для каждого запроса, и не заметил, что также необходимо поддерживать вещь TimeoutException .

Я надеюсь, что это будет рассмотрено в будущем.

Что бы это ни стоило, я также считаю, что TaskCanceledException , выбрасываемое по тайм-аутам, сбивает с толку.

Я могу засвидетельствовать, что TaskCanceledExceptions были основным источником путаницы при решении производственных вопросов на моей предыдущей работе. Обычно мы оборачиваем вызовы HttpClient и выбрасываем исключения TimeoutException, где это уместно.

@eiriktsarpalis, спасибо. Мы планируем решить эту проблему в версии 5.0, так что вы сможете забрать ее в те же сроки, если хотите, теперь, когда вы присоединились к нашей команде 😇.

1 год? 😵 Где обещанная гибкость .NET Core?

Судить о гибкости всего проекта по одной проблеме немного... несправедливо.
Да, это немного смущает, у нас все еще есть эта проблема, однако ее трудно исправить (с влиянием совместимости), и сначала нужно было решить более высокие бизнес-цели (в сети) - см. Изменения вехи на протяжении всей истории этого вопроса.

Кстати: как и во многих других проектах OSS, нет SLA, ETA или обещаний, когда проблема будет исправлена. Все зависит от приоритетов, другой работы, сложности и количества специалистов, способных решить проблему.

Судить о гибкости всего проекта по одной проблеме немного... несправедливо.

Что ж, это был честный вопрос. Прошло уже два года, и когда я увидел, что он собирается взять еще один, я был немного удивлен. Хотя без обид 🙂. Я знаю, что для того, чтобы решить эту проблему, было/есть несколько вопросов, на которые нужно было/нужно ответить в первую очередь.

В мире компромиссов... Мне это не нравится, но вот еще один вариант для разжевывания:

Мы могли бы продолжать выбрасывать TaskCanceledException , но установить для CancellationToken сторожевое значение.

c# catch(TaskCanceledException ex) when(ex.CancellationToken == HttpClient.TimeoutToken) { }

И еще: добавьте новый HttpTimeoutException , который является производным от TaskCanceledException .

И еще: добавьте новый HttpTimeoutException , который является производным от TaskCanceledException .

Думаю, это решит все проблемы. Это упрощает специальное перехват тайм-аута, и это по-прежнему TaskCanceledException , поэтому код, который в настоящее время перехватывает это исключение, также перехватывает новое (без критических изменений).

И еще: сгенерируйте новое исключение HttpTimeoutException, которое является производным от TaskCanceledException.

Это выглядит довольно некрасиво, но, вероятно, это хороший компромисс.

И еще: добавьте новый HttpTimeoutException , который является производным от TaskCanceledException .

Думаю, это решит все проблемы. Это упрощает специальное перехват тайм-аута, и это по-прежнему TaskCanceledException , поэтому код, который в настоящее время перехватывает это исключение, также перехватывает новое (без критических изменений).

Что ж, _могут_ быть люди, которые пишут такой код:

try
{
  // ...
}
catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException))
{
}

Так что технически это было бы критическим изменением.

Также я думаю, что в нем есть немного худшего из обоих миров. Мы все равно не получим общие TimeoutException одновременно с критическим изменением.

могут быть люди, которые пишут такой код, так что технически это будет критическое изменение

FWIW, почти каждое изменение, возможно, ломается, поэтому, чтобы добиться прогресса в чем-либо, нужно где-то провести черту. В corefx мы не считаем возврат или выбрасывание более производного типа нарушением.
https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/breaking-change-rules.md#exceptions

В мире компромиссов... Мне это не нравится, но вот еще один вариант для разжевывания:

Мы могли бы продолжать выбрасывать TaskCanceledException , но установить для CancellationToken сторожевое значение.

catch(TaskCanceledException ex) when(ex.CancellationToken == HttpClient.TimeoutToken)
{
}

Мне очень нравится этот. Есть ли у него какой-либо другой недостаток, кроме того, что он "некрасивый"? Что такое ex.CancellationToken сегодня, когда происходит тайм-аут?

Есть ли у него какие-либо другие недостатки, кроме того, что он "некрасивый"?

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

Мне очень нравится этот. Есть ли у него какой-либо другой недостаток, кроме того, что он "некрасивый"? Что такое ex.CancellationToken сегодня, когда происходит тайм-аут?

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

Я привык считать, что OperationCanceledException и производные типы обозначают прерванное выполнение, а не ошибку приложения. Исключение, которое выдает HttpClient по тайм-ауту, обычно является ошибкой — был вызов HTTP API, и он был недоступен. Такое поведение либо приводит к осложнениям при обработке исключений на более высоком уровне, например, в промежуточном программном обеспечении конвейера ASP.NET Core, поскольку этот код должен знать, что существует две разновидности OperationCanceledException , ошибка и не ошибка, или это заставляет вас заключать каждый вызов HttpClient в блок throw-catch.

И еще: сгенерируйте новое исключение HttpTimeoutException, которое является производным от TaskCanceledException.

Это выглядит довольно некрасиво, но, вероятно, это хороший компромисс.

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

Мы хотим взглянуть на это глубже, чем просто изменить тип исключения. Существует также давняя проблема «где мы потеряли тайм-аут?» -- например, запрос может истечь по тайм-ауту, даже не достигнув связи, -- на что нам нужно обратить внимание, и решение должно быть совместимо с направлением, в котором мы идем.

Мы хотим взглянуть на это глубже, чем просто изменить тип исключения. Существует также давняя проблема «где мы потеряли тайм-аут?» -- например, запрос может истечь по тайм-ауту, даже не достигнув связи, -- на что нам нужно обратить внимание, и решение должно быть совместимо с направлением, в котором мы идем.

Это действительно то, что вам нужно знать? Я имею в виду, будете ли вы обрабатывать ошибку по-разному в зависимости от того, где истекло время ожидания? Для меня обычно достаточно просто знать, что произошел тайм-аут.
Я не против, если вы захотите сделать больше, я просто не хочу, чтобы это изменение пропустило поезд .NET 5 из-за дополнительных требований :wink:

Я думаю, что проблема , поднятая @scalablecory , заслуживает внимания, но я согласен, что это не должно мешать скорейшему исправлению. Вы можете выбрать механизм, позволяющий оценить причину тайм-аута, но, пожалуйста, не откладывайте исправление проблемы с исключением для этого... вы всегда можете добавить свойство Reason к исключению позже.

Я согласен с @thomaslevesque; Раньше я не слышал, чтобы это было большой проблемой. Откуда это?

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

Мы хотим взглянуть на это глубже, чем просто изменить тип исключения. Существует также давняя проблема «где мы потеряли тайм-аут?» -- например, запрос может истечь по тайм-ауту, даже не достигнув связи, -- на что нам нужно обратить внимание, и решение должно быть совместимо с направлением, в котором мы идем.

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

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

Я не понимаю, как это возможно с абстракцией, определенной сегодня. HttpMessageHandler ничего не знает о времени ожидания HttpClient; что касается HttpMessageHandler, он только что передал токен отмены, который может запрашивать отмену по любому количеству причин, включая сигнализацию токена отмены вызывающей стороны, вызов CancelPendingRequests HttpClient, принудительное истечение времени ожидания HttpClient и т. д.

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

Полезно знать, обработал ли сервер ваш запрос. Это также отлично подходит для диагностики — удаленный сервер не истекает по тайм-ауту, но клиент сообщает о тайм-ауте — это разочаровывает.

У @davidsh может быть больше информации.

Я согласен с @thomaslevesque; Раньше я не слышал, чтобы это было большой проблемой. Откуда это?

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

@dotnet/ncl @stephentoub Давай уложим это в постель. Мы знаем, что все, что мы делаем, будет незаметно ломаться. Это кажется наименее худшим вариантом. Предлагаю двигаться дальше:

бросить новый HttpTimeoutException , который происходит от TaskCanceledException .

Возражения?

создать новое исключение HttpTimeoutException, которое является производным от TaskCanceledException.

Как мы это делаем на практике? Это, вероятно, означает, что мы должны очистить наш код и найти все места, которые вызывают CancellationToken.ThrowIfCancellationRequested , и вместо этого преобразовать его в:

c# if (token.IsCancellationRequested) throw new HttpTimeoutException(EXTRASTUFF, token);

а также явные места, которые мы могли бы на самом деле вызывать OperationCancelledException .

Есть ли какой-нибудь вариант, который в любом случае не требует просмотра всего этого связанного кода?

Есть ли какой-нибудь вариант, который в любом случае не требует просмотра всего этого связанного кода?

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

Это, вероятно, означает, что мы должны очистить наш код и найти все места, которые вызывают CancellationToken.ThrowIfCancellationRequested, и вместо этого преобразовать его в:

Есть ли какой-нибудь вариант, который в любом случае не требует просмотра всего этого связанного кода?

Не совсем.

Что касается обработчиков, все, что они получают, — это CancellationToken: они не знают, и им все равно, откуда взялся этот токен, кто его произвел или почему он может запросить отмену. Все, что они знают, это то, что в какой-то момент может быть запрошена отмена, и в ответ они выдают исключение отмены. Так что в обработчиках тут делать нечего.

Сам HttpClient создает соответствующий CancellationTokenSource и устанавливает для него тайм-аут:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L488
Тайм-аут — это полностью изобретение самого HttpClient, и только он знает, захочет ли он обрабатывать возникшее исключение отмены как особое.

Это означает, что указанный выше код необходимо будет реорганизовать, чтобы отслеживать тайм-аут отдельно, либо как отдельный CancellationTokenSource, либо, что более вероятно, самостоятельно обрабатывая логику тайм-аута, и в дополнение к отмене CTS, также устанавливая флаг, который он может тогда тоже проверь. Это, скорее всего, не будет платным, поскольку мы добавим здесь дополнительные распределения, но с точки зрения кода это относительно просто.

Затем в таких местах в HttpClient, как:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L536 -L541
и
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L562 -L566
когда он перехватывает исключение, ему необходимо выполнить отмену в особом случае, чтобы проверить, истек ли тайм-аут, и, если он истек, предположить, что это было причиной отмены, и создать новое исключение нужного типа.

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

Как это работает, если свойство HttpClient.Timeout не задействовано? Возможно, он установлен на «бесконечность». Как насчет токенов отмены, передаваемых непосредственно в API-интерфейсы HttpClient (GetAsync, SendAync и т. д.)? Я предполагаю, что это будет работать так же?

Как это работает, если свойство HttpClient.Timeout не задействовано?

Я не понимаю вопроса... это единственный тайм-аут, о котором мы здесь говорим. Если установлено значение Бесконечно, то отмена никогда не будет вызвана этим тайм-аутом, и ничего нельзя будет сделать. Мы уже используем особый случай Timeout == Infinite и будем продолжать:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L481

Как насчет токенов отмены, передаваемых непосредственно в API-интерфейсы HttpClient (GetAsync, SendAync и т. д.)? Я предполагаю, что это будет работать так же?

Мы объединяем любой переданный токен с одним из наших собственных решений:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L485
Мы будем отслеживать нашу собственную обработку HttpClient.Timeout, как упоминалось в моих предыдущих комментариях. Если вызывающая сторона решает передать токен, который он настроил с тайм-аутом, то особая обработка зависит от него: с нашей точки зрения, это просто запрос на отмену.

Возражения?

Зачем вводить новый производный тип исключения, который «конкурирует» с существующим TimeoutException, а не, скажем, генерировать исключение отмены, которое оборачивает TimeoutException в качестве внутреннего исключения? Если цель состоит в том, чтобы позволить потребителю программно определить, была ли отмена вызвана тайм-аутом, будет ли этого достаточно? Казалось бы, он передает ту же информацию, не нуждаясь в новой площади поверхности.

Зачем вводить новый производный тип исключения, который «конкурирует» с существующим TimeoutException, вместо того, чтобы, скажем, генерировать исключение отмены, которое оборачивает TimeoutException в качестве внутреннего исключения

Для удобства, в основном. Писать проще и понятнее

catch(HttpTimeoutException)

чем

catch(OperationCanceledException ex) when (ex.InnerException is TimeoutException)

При этом меня устраивает любой способ, если достаточно легко определить тайм-аут.

скажем, выбрасывая исключение отмены, которое оборачивает TimeoutException как внутреннее исключение?

Я думаю, это хороший компромисс. Это помогло бы устранить неоднозначность журналов исключений, которые были моим основным источником раздражения при работе с такими тайм-аутами. Вероятно, это также лучше сообщает тот факт, что это HttpClient, отменяющий запрос, а не тайм-аут, инициированный базовым обработчиком. Также никаких критических изменений.

Сортировка:

  • Мы обновим этот сценарий, чтобы иметь InnerException из TimeoutException с сообщением для регистрации/диагностики, которое легко определяет отмену как вызванную срабатыванием тайм-аута.
  • Мы будем обновлять документацию, добавляя рекомендации о том, как можно программно проверить, чтобы различать отмену: проверка на TimeoutException , использование бесконечного Timeout и передача CancellationToken с периодом ожидания и т. д.

Мы понимаем, что любое решение здесь связано с некоторым компромиссом, и считаем, что это обеспечит хороший уровень совместимости с существующим кодом.

Зачем вводить новый производный тип исключения, который «конкурирует» с существующим TimeoutException, вместо того, чтобы, скажем, генерировать исключение отмены, которое оборачивает TimeoutException в качестве внутреннего исключения

Для удобства, в основном. Писать проще и понятнее

catch(HttpTimeoutException)

чем

catch(OperationCanceledException ex) when (ex.InnerException is TimeoutException)

При этом меня устраивает любой способ, если достаточно легко определить тайм-аут.

@scalablecory Я расстроен тем, что, похоже, пропустил обсуждение на несколько часов - мой опыт общения с разработчиками LoB .NET общего типа заключается в том, что поймать производное исключение HttpTimeoutException людям намного проще понять / принять, чем необходимость не забудьте использовать фильтр исключений для внутреннего исключения. Не все даже знают, что такое фильтр исключений.

В то же время я полностью осознаю, что это один из тех сценариев, где нет однозначного выигрышного/бескомпромиссного решения, как вы сказали.

Мы согласились с этим, @ericsampson , но это было бы серьезным изменением для многих существующих пользователей. Как упомянул @scalablecory , мы пошли на компромисс, чтобы внести улучшения, ограничивая ущерб совместимости.

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

Мы согласились с этим, @ericsampson , но это было бы серьезным изменением для многих существующих пользователей. Как упомянул @scalablecory , мы пошли на компромисс, чтобы внести улучшения, ограничивая ущерб совместимости.

Хотя я не обязательно не согласен с вариантом упаковки, я думаю, стоит указать, что, насколько я понял, проблема не в том, что производный вариант является критическим изменением, а в том, что вариант упаковки проще. Как упоминалось ранее в обсуждении @stephentoub , создание производного исключения не считается критическим изменением в соответствии с рекомендациями corefx.

Получение HttpTimeoutException от TaskCanceledException нарушает отношение «является» между этими двумя исключениями. Вызывающий код по-прежнему будет думать, что отмена произошла, если только он специально не обрабатывает HttpTimeoutException. По сути, это та же самая обработка, которую мы должны выполнять в момент, когда мы проверяем отмену токена отмены, чтобы определить, истек ли он по тайм-ауту или нет. Кроме того, наличие двух исключений тайм-аута в основной библиотеке сбивает с толку.
Добавление TimeoutException в качестве внутреннего исключения в TaskCanceledException в большинстве случаев не предоставит никакой дополнительной информации, поскольку мы уже можем проверить токен отмены и определить, истек ли он по таймауту или нет.

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

Можно ли обновить документацию для существующих версий, чтобы указать, что методы могут выдавать TaskCanceledException и/или OperationCanceledException (оба конкретных типа возможны) при истечении времени ожидания? В настоящее время в документации указано, что HttpRequestException имеет дело с тайм-аутами, что неверно и вводит в заблуждение.

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

Спасибо @alnikola , @scalablecory , @davidsh, @karelz и всем, кто участвовал в обсуждении/реализации, я очень ценю это.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги