2

My usage scenario: I have a lot of Url links, and I will request them regularly to judge whether the service is normal. And the status of these URLs will be displayed on the client side

Problem: If one of the Urls times out, the other Urls will wait together. How can I handle this situation? such as not blocking other requests after Url1 times out (other urls will return results first).

How to return the results of parallel runs through the API (Or keep returning results)? Let the client display more friendly. such as returning the normal URL result first, and then returning the timeout result after waiting for a timeout.

The following Test1 and Test2 are my two attempts, but none of them have achieved the desired effect.

string[] urls = new string[]
{
    "https://localhost:7268/api/Timeout",
    "https://www.google.com:81",
    "https://url1",
    "https://url2"
};

// Get Data
async Task<HttpResponseMessage> HttpRequest2Async(string url, CancellationToken cancellationToken = default)
{
    HttpClient httpClient = new HttpClient();
    
    var responseMessage = await httpClient.GetAsync(url, cancellationToken);
    
    return responseMessage;
}

// Test1 When Url1 times out, other Urls behind will wait.
List<HttpResponseMessage> urlResponses = new();

foreach (string url in urls)
{
    var responseMessage = await HttpRequest2Async(url, default);
    
    urlResponses.Add(responseMessage);
}

// Test2 As long as there is a Url timeout, all Urls will wait.
var tasks = new List<Task<HttpResponseMessage>>();

foreach (string url in urls)
{
    var task = HttpRequest2Async(url, default);
    
    tasks.Add(task);
}

var res = await Task.WhenAll(tasks);

Result:

The Url behind https://localhost:7268/api/Timeout will wait for it to complete before continuing to execute. enter image description here

All URLs will wait for https://localhost:7268/api/Timeout to complete before returning the result. enter image description here

SBANAS
  • 31
  • 3
  • 1
    [How to download files using HttpClient with a ProgressBar?](https://stackoverflow.com/a/74554087/7444103) -- [You're using HttpClient wrong ...](https://www.aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/) – Jimi Nov 29 '22 at 05:33
  • 1
    The `HttpClient` class is intended to be instantiated [once](https://learn.microsoft.com/en-us/aspnet/web-api/overview/advanced/calling-a-web-api-from-a-net-client#create-and-initialize-httpclient), and reused throughout the life of an application. – Theodor Zoulias Nov 29 '22 at 05:33
  • Youre waiting til "WhenAll" are complete. Maybe you could send out a job to run in background independent of request- check hangfire. – matt sharp Nov 29 '22 at 05:41
  • Also you can use HttpClient however you want Theodor – matt sharp Nov 29 '22 at 05:45
  • 2
    @mattsharp yes, you can use it however you want, and then suffer the consequences (`SocketException` errors). – Theodor Zoulias Nov 29 '22 at 06:01
  • You mean scoped tho - this is the point I was getting at NOT instantiated once in whole application. – matt sharp Nov 29 '22 at 10:45
  • *"If one of the Urls times out, the other Urls will wait together."* -- Could you elaborate on this? The problem is not clear. Also please mention the version of the .NET platform that you are using, and whether you are running it with or without the debugger attached. – Theodor Zoulias Nov 30 '22 at 10:37
  • 1
    @Jimi Thanks a lot, turns out it was a parallel problem. – SBANAS Dec 01 '22 at 03:23
  • 1
    @TheodorZoulias Also thanks for pointing out my "instantiated once" problem, I've added more info. – SBANAS Dec 01 '22 at 03:23

1 Answers1

1

If one of the Urls times out, the other Urls will wait together.

^ This is imprecise. A more precise way to put it is: Because the code is organised in a way that all http responses are collected before any of them is processed, slow requests block processing, (and this is most visible if any of requests results in a time out, because it takes aages before the request times out).

I can see 4 options:

  1. Reorganise the code to not wait for all the responses before processing them. Something like:
    ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = n }; 
    await Parallel.ForEachAsync(urls, parallelOptions, async (uri, cancellationToken) =>
    {
        var r = await client.GetAsync(uri, cancellationToken);
     
        ... process the response here, not after all GetAsyncs are finished
    });
    
    The point here is the for all {fetch;process} (vs for all {fetch}; for all {process}).
  2. Use Task.WhenAny() instead of WhenAll and start processing the tasks as they finish.
  3. Use a more aggressive global timeout: HttpClient.Timeout Property
  4. Use a more aggressive timeout per request (using a CancellationTokenSource and passing a cancellationToken to GetAsync)
tymtam
  • 31,798
  • 8
  • 86
  • 126
  • Thanks a lot, the first way should be what I need, but how do I return these results in the API? Does .NET API support continuously returning results? – SBANAS Dec 01 '22 at 03:51
  • @SBANAS it wouldn't need to do that if you set a more aggressive timeout. – mason Dec 01 '22 at 03:56
  • @SBANAS for a variant of the `Parallel.ForEachAsync` that returns the results as an array, you could look [here](https://stackoverflow.com/questions/30907650/foreachasync-with-result "ForEachAsync with Result"). – Theodor Zoulias Dec 01 '22 at 03:57