HttpClient асинхронные запросы не завершаются для большого пакета, отправленного в цикле
Я думаю, что мне удалось сделать тест, который неоднократно показывает эту проблему, по крайней мере, в моей системе. Этот вопрос касается использования HttpClient для плохой конечной точки (несуществующая конечная точка, цель не работает).
Проблема в том, что количество выполненных задач отстает от общего числа, обычно примерно на несколько. Я не против, чтобы запросы не работали, но это просто приводит к тому, что приложение просто зависает, когда ожидаются результаты.
Я получаю следующий результат из кода теста ниже:
Прошло: 237.2009884 секунд. Задачи в массиве пакетов: 8000 Выполнено задач: 7993
Если я устанавливаю размер пакета 8 вместо 8000, он завершается. Для 8000 это застревает на Когда.
Интересно, получат ли другие люди такой же результат, если я делаю что-то не так, и кажется ли это ошибкой?
using System;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace CustomArrayTesting
{
/// <summary>
/// Problem: a large batch of async http requests is done in a loop using HttpClient, and a few of them never complete
/// </summary>
class ProgramTestHttpClient
{
static readonly int batchSize = 8000; //large batch size brings about the problem
static readonly Uri Target = new Uri("http://localhost:8080/BadAddress");
static TimeSpan httpClientTimeout = TimeSpan.FromSeconds(3); // short Timeout seems to bring about the problem.
/// <summary>
/// Sends off a bunch of async httpRequests using a loop, and then waits for the batch of requests to finish.
/// I installed asp.net web api client libraries Nuget package.
/// </summary>
static void Main(String[] args)
{
httpClient.Timeout = httpClientTimeout;
stopWatch = new Stopwatch();
stopWatch.Start();
// this timer updates the screen with the number of completed tasks in the batch (See timerAction method bellow Main)
TimerCallback _timerAction = timerAction;
TimerCallback _resetTimer = ResetTimer;
TimerCallback _timerCallback = _timerAction + _resetTimer;
timer = new Timer(_timerCallback, null, TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan);
//
for (int i = 0; i < batchSize; i++)
{
Task<HttpResponseMessage> _response = httpClient.PostAsJsonAsync<Object>(Target, new Object());//WatchRequestBody()
Batch[i] = _response;
}
try
{
Task.WhenAll(Batch).Wait();
}
catch (Exception ex)
{
}
timer.Dispose();
timerAction(null);
stopWatch.Stop();
Console.WriteLine("Done");
Console.ReadLine();
}
static readonly TimeSpan timerRepeat = TimeSpan.FromSeconds(1);
static readonly HttpClient httpClient = new HttpClient();
static Stopwatch stopWatch;
static System.Threading.Timer timer;
static readonly Task[] Batch = new Task[batchSize];
static void timerAction(Object state)
{
Console.Clear();
Console.WriteLine("Elapsed: {0} seconds.", stopWatch.Elapsed.TotalSeconds);
var _tasks = from _task in Batch where _task != null select _task;
int _tasksCount = _tasks.Count();
var _completedTasks = from __task in _tasks where __task.IsCompleted select __task;
int _completedTasksCount = _completedTasks.Count();
Console.WriteLine("Tasks in batch array: {0} Completed Tasks : {1} ", _tasksCount, _completedTasksCount);
}
static void ResetTimer(Object state)
{
timer.Change(timerRepeat, Timeout.InfiniteTimeSpan);
}
}
}
Иногда происходит сбой перед завершением обработки необработанного исключения Access Violation. Стек вызовов просто говорит:
> mscorlib.dll!System.Threading._IOCompletionCallback.PerformIOCompletionCallback(uint errorCode = 1225, uint numBytes = 0, System.Threading.NativeOverlapped* pOVERLAP = 0x08b38b98)
[Native to Managed Transition]
kernel32.dll!@BaseThreadInitThunk@12()
ntdll.dll!___RtlUserThreadStart@8()
ntdll.dll!__RtlUserThreadStart@8()
Большую часть времени он не дает сбой, но просто не заканчивает ожидание когда все. В любом случае для каждого запроса генерируются следующие исключения из первого шанса:
A first chance exception of type 'System.Net.Sockets.SocketException' occurred in System.dll
A first chance exception of type 'System.Net.WebException' occurred in System.dll
A first chance exception of type 'System.AggregateException' occurred in mscorlib.dll
A first chance exception of type 'System.ObjectDisposedException' occurred in System.dll
Я сделал остановку отладчика на исключении Object и получил этот стек вызовов:
> System.dll!System.Net.Sockets.NetworkStream.UnsafeBeginWrite(byte[] buffer, int offset, int size, System.AsyncCallback callback, object state) + 0x136 bytes
System.dll!System.Net.PooledStream.UnsafeBeginWrite(byte[] buffer, int offset, int size, System.AsyncCallback callback, object state) + 0x19 bytes
System.dll!System.Net.ConnectStream.WriteHeaders(bool async = true) + 0x105 bytes
System.dll!System.Net.HttpWebRequest.EndSubmitRequest() + 0x8a bytes
System.dll!System.Net.HttpWebRequest.SetRequestSubmitDone(System.Net.ConnectStream submitStream) + 0x11d bytes
System.dll!System.Net.Connection.CompleteConnection(bool async, System.Net.HttpWebRequest request = {System.Net.HttpWebRequest}) + 0x16c bytes
System.dll!System.Net.Connection.CompleteConnectionWrapper(object request, object state) + 0x4e bytes
System.dll!System.Net.PooledStream.ConnectionCallback(object owningObject, System.Exception e, System.Net.Sockets.Socket socket, System.Net.IPAddress address) + 0xf0 bytes
System.dll!System.Net.ServicePoint.ConnectSocketCallback(System.IAsyncResult asyncResult) + 0xe6 bytes
System.dll!System.Net.LazyAsyncResult.Complete(System.IntPtr userToken) + 0x65 bytes
System.dll!System.Net.ContextAwareResult.Complete(System.IntPtr userToken) + 0x92 bytes
System.dll!System.Net.LazyAsyncResult.ProtectedInvokeCallback(object result, System.IntPtr userToken) + 0xa6 bytes
System.dll!System.Net.Sockets.BaseOverlappedAsyncResult.CompletionPortCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* nativeOverlapped) + 0x98 bytes
mscorlib.dll!System.Threading._IOCompletionCallback.PerformIOCompletionCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* pOVERLAP) + 0x6e bytes
[Native to Managed Transition]
Сообщение об исключении было:
{"Cannot access a disposed object.\r\nObject name: 'System.Net.Sockets.NetworkStream'."} System.Exception {System.ObjectDisposedException}
Обратите внимание на связь с этим необработанным исключением нарушения прав доступа, которое я редко вижу.
Таким образом, кажется, что HttpClient не является устойчивым, когда цель не работает. Я делаю это на Windows 7 32, кстати.
3 ответа
Я просмотрел источник HttpClient с помощью рефлектора. Насколько я вижу, для синхронно выполняемой части операции (когда она запускается), похоже, что к возвращенной задаче не применен тайм-аут. Существует некоторая реализация тайм-аута, которая вызывает Abort() для объекта HttpWebRequest, но, опять же, они, похоже, пропустили любую отмену тайм-аута или ошибку возвращаемого задания на этой стороне асинхронной функции. Там может быть что-то на стороне обратного вызова, но иногда обратный вызов, вероятно, "пропадает", что приводит к тому, что возвращаемая задача никогда не завершается.
Я опубликовал вопрос, спрашивающий, как добавить время ожидания для любой Задачи, и ответчик дал это очень хорошее решение (здесь, в качестве метода расширения):
public static Task<T> WithTimeout<T>(this Task<T> task, TimeSpan timeout)
{
var delay = task.ContinueWith(t => t.Result
, new CancellationTokenSource(timeout).Token);
return Task.WhenAny(task, delay).Unwrap();
}
Таким образом, вызов HttpClient, подобный этому, должен предотвращать бесконечное выполнение "плохих задач":
Task<HttpResponseMessage> _response = httpClient.PostAsJsonAsync<Object>(Target, new Object()).WithTimeout<HttpResponseMessage>(httpClient.Timeout);
Еще пара вещей, которые, я думаю, делали запросы менее вероятными пропавшими без вести: 1. Увеличение времени ожидания с 3 до 30 секунд привело к завершению всех задач в программе, которую я разместил с этим вопросом. 2. Увеличение количества одновременных подключений, разрешенных с использованием, например, System.Net.ServicePointManager.DefaultConnectionLimit = 100;
Я сталкивался с этим вопросом, когда искал решения аналогичной проблемы от WCF. Эта серия исключений точно такая же, как я вижу. В конце концов, после множества исследований я обнаружил ошибку в HttpWebRequest, которую использует HttpClient. HttpWebRequest находится в плохом состоянии и отправляет только заголовки HTTP. Затем он ожидает ответа, который никогда не будет отправлен.
Я собрал билет с помощью Microsoft Connect, который можно найти здесь: https://connect.microsoft.com/VisualStudio/feedback/details/1805955/async-post-httpwebrequest-hangs-when-a-socketexception-occurs-during-setsocketoption
Подробности указаны в заявке, но для этого требуется асинхронный вызов POST из HttpWebRequest на нелокальный хост-компьютер. Я воспроизвел это на Windows 7 в.Net 4.5 и 4.6. Неудачный вызов SetSocketOption, который вызывает исключение SocketException, не выполняется только в Windows 7 при тестировании.
Для нас параметр UseNagleAlgorithm вызывает вызов SetSocketOption, но мы не можем избежать его, так как WCF отключает UseNagleAlgorithm, и вы не можете его остановить. В WCF это выглядит как время ожидания вызова. Очевидно, что это не так здорово, так как мы проводим 60-е, ничего не ожидая.
Ваша информация об исключении теряется в WhenAll
задача. Вместо того, чтобы использовать это, попробуйте это:
Task aggregateTask = Task.Factory.ContinueWhenAll(
Batch,
TaskExtrasExtensions.PropagateExceptions,
TaskContinuationOptions.ExecuteSynchronously);
aggregateTask.Wait();
Это использует PropagateExceptions
метод расширения из примера кода Parallel Extensions Extras, чтобы гарантировать, что информация об исключениях из задач в пакетной операции не будет потеряна:
/// <summary>Propagates any exceptions that occurred on the specified tasks.</summary>
/// <param name="tasks">The Task instances whose exceptions are to be propagated.</param>
public static void PropagateExceptions(this Task [] tasks)
{
if (tasks == null) throw new ArgumentNullException("tasks");
if (tasks.Any(t => t == null)) throw new ArgumentException("tasks");
if (tasks.Any(t => !t.IsCompleted)) throw new InvalidOperationException("A task has not completed.");
Task.WaitAll(tasks);
}