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);
}
Другие вопросы по тегам