1

I have an API that must call in parallel 4 HttpClients supporting a concurrency of 500 users per second (all of them calling the API at the same time)

There must be a strict timeout letting the API to return a result even if not all the HttpClients calls have returned a value.
The endpoints are external third party APIs and I don't have any control on them or know the code.
I did extensive research on the matter, but even if many solution works, I need the one that consume less CPU as possible since I have a low server budget.

So far I came up with this:

var conn0 = new HttpClient
{
    Timeout = TimeSpan.FromMilliseconds(1000),
    BaseAddress = new Uri("http://endpoint")
};
var conn1 = new HttpClient
{
    Timeout = TimeSpan.FromMilliseconds(1000),
    BaseAddress = new Uri("http://endpoint")
};
var conn2 = new HttpClient
{
    Timeout = TimeSpan.FromMilliseconds(1000),
    BaseAddress = new Uri("http://endpoint")
};
var conn3 = new HttpClient
{
    Timeout = TimeSpan.FromMilliseconds(1000),
    BaseAddress = new Uri("http://endpoint")
};

var list = new List<HttpClient>() { conn0, conn1, conn2, conn3 };
var timeout = TimeSpan.FromMilliseconds(1000);
var allTasks = new List<Task<Task>>();

//the async DoCall method just call the HttpClient endpoint and return a MyResponse object
foreach (var call in list)
{
    allTasks.Add(Task.WhenAny(DoCall(call), Task.Delay(timeout)));
}
var completedTasks = await Task.WhenAll(allTasks);
var allResults = completedTasks.OfType<Task<MyResponse>>().Select(task => task.Result).ToList();
return allResults;

I use WhenAny and two tasks, one for the call, one for the timeout.If the call task is late, the other one return anyway.

Now, this code works perfectly and everything is async, but I wonder if there is a better way of achieving this.
Ever single call to this API creates lot of threads and with 500concurrent users it needs an avarage of 8(eight) D3_V2 Azure 4-core machines resulting in crazy expenses, and the higher the timeout is, the higher the CPU use is.

Is there a better way to do this without using so many CPU resources (maybe Parallel Linq a better choice than this)?

Is the HttpClient timeout alone sufficient to stop the call and return if the endpoint do not reply in time, without having to use the second task in WhenAny?

UPDATE:

  • The endpoints are third party APIs, I don't know the code or have any control, the call is done in JSON and return JSON or a string.
  • Some of them reply after 10+ seconds once in a while or got stuck and are extremely slow,so the timeout is to free the threads and return even if with partial data from the other that returned in time.
  • Caching is possible but only partially since the data change all the time, like stocks and forex real time currency trading.
Francesco Cristallo
  • 2,856
  • 5
  • 28
  • 57
  • Can you provide more information about the type and nature of the endpoints that you are calling, is caching part of the results viable in your use case, why is there a hard timeout (it seems to be cost driven but are there any other reasons), do you have access to the endpoints' code and infrastructure or are you calling a 3rd party api? – univ Feb 20 '17 at 20:31
  • I added some info to the question. I would like to know if there is something wrong in how is now implemented and if is possible to do better than that due to the variety of parallel/concurrency solutions that .net have – Francesco Cristallo Feb 20 '17 at 20:53

1 Answers1

2

Your approach using the two tasks just for timeout do work, but you can do a better thing: use CancellationToken for the task, and for getting the answers from a server:

var cts = new CancellationTokenSource();
// set the timeout equal to the 1 second
cts.CancelAfter(1000);
// provide the token for your request
var response = await client.GetAsync(url, cts.Token);  

After that, you simply can filter the completed tasks:

var allResults = completedTasks
    .Where(t => t.IsCompleted)
    .Select(task => task.Result).ToList();

This approach will decrease the number of tasks you're creating no less than two times, and will decrease the overhead on your server. Also, it will provide you a simple way to cancel some part of the handling or even whole one. If your tasks are completely independent from each other, you may use a Parallel.For for calling the http clients, yet still usee the token for cancelling the operation:

ParallelLoopResult result = Parallel.For(list, call => DoCall(call, cts.Token));
// handle the result of the parallel tasks

or, using the PLINQ:

var results = list
    .AsParallel()
    .Select(call => DoCall(call, cts.Token))
    .ToList();
VMAtm
  • 27,943
  • 17
  • 79
  • 125
  • I tried the cancellationtoken approach, but the cancellation token is not always respected from the Httpclient. I did some researches and ended up having this problem almost all the time: http://stackoverflow.com/questions/29319086/cancelling-an-httpclient-request-why-is-taskcanceledexception-cancellationtoke The httpclient create a new cancellationToken and try to complete the action that in case takes let's say 10seconds, block all the API for this amount of time. This is why I went for a strict two task solution, but it's too computational intensive. – Francesco Cristallo Feb 20 '17 at 21:18
  • I will investigate and try the PLINQ approach and post an update, thanks for your help. – Francesco Cristallo Feb 20 '17 at 21:19