0

I am working to reduce latency and increase responsiveness for my application. Within on of my API Controllers (Asp .NET Core)

I am using an HttpClient to perform several requests to a server, fetching incident and task information.

I perform two requests per group a user is associated with (I fetch this information using Microsoft Graph API); one for incidents, one for tasks.

I then await their results and if they do not return null, append them to my dictionary object under the group's name.

Return Object:

Group 1 
    -- Incidents ArrayList
    -- Tasks ArrayList

Group 2 
    -- Incidents ArrayList
    -- Tasks ArrayList
...

My code is as follows:

Dictionary<string, ArrayList> final;
Microsoft.Graph.Group group_details;
string group_name;

using (var client = ClientFactory.GetClientAsync(_apiCredentials))
{
    //for each group send request to get past 30 day's incidents and tasks
    foreach (var group in groups.CurrentPage)
    {
        //request to Graph API to get name of group for query
        try
        {
            group_details = await _graphServiceClient.Groups[group.Id].Request()
                .GetAsync();
            group_name = group_details.DisplayName.Replace("#", "").Trim();

            //creates async tasks for fetching Incidents and Tasks
            var getIncidentsTask = GetIncidents(client, startDate, group_name);
            var getTasksTask = GetTasks(client, startDate, group_name);

            //awaits values
            var Incidents = await getIncidentsTask;
            var Tasks = await getTasksTask;

            //if both Incidents and Tasks exist for current group
            if (Incidents != null && Tasks != null)
            {
                final.Add(group_name, new ArrayList());
                final[group_name].Add(Incidents);
                final[group_name].Add(Tasks);
            }
        }
        catch(Exception e)
        {
            _logger.LogError(e.Message);
        }
    }
}

I know there exists a more advanced way to await these tasks. I was attempting to utilize Task.WhenAll(), however this will only work with an IEnumerable so I have to dig a layer in and loop through each group while using this statement to generate a task per completion of each set of incidents/tasks.

My goal is to have all tasks run to completion in the background while as few threads are blocked as possible, then if each set of results are not null, append them to my return object.

If anyone has advice on this I would be very appreciative.

Noah Eldreth
  • 77
  • 10
  • What makes you think `Task.WhenAll` only works with IEnumerable? There is a `public static System.Threading.Tasks.Task WhenAll (params System.Threading.Tasks.Task[] tasks);` overload that will allow you to do `await Task.WhenAll(getIncidentsTask, getTasksTask);` without issue. That said there are differences between the two ways of doing this (when exceptions occur), I will refer you to this question to learn more about that: https://stackoverflow.com/q/18310996/10608418 –  Dec 07 '20 at 15:22
  • I might have been unclear. I meant that as far as I cannot use this function with my dictionary object and await everything in that one line. – Noah Eldreth Dec 07 '20 at 15:29
  • 2
    Ah I see. After you've used `Task.WhenAll` you can just do `var Incidents = getIndidentsTask.Result` or even do another `var Incidents = await getIncidentsTask;`. –  Dec 07 '20 at 15:44
  • Could you add the definition of the `final` variable? – Theodor Zoulias Dec 08 '20 at 05:59
  • Also are you sure that you want to start processing all groups at once, without imposing any [limitation](https://stackoverflow.com/questions/10806951/how-to-limit-the-amount-of-concurrent-async-i-o-operations/) to the maximum concurrency? – Theodor Zoulias Dec 08 '20 at 06:03
  • 1
    You should also take a look at this: [How to increase the outgoing HTTP requests quota in .NET Core?](https://stackoverflow.com/questions/55372354/how-to-increase-outgoing-http-requets-quota-in-net-core) – Theodor Zoulias Dec 08 '20 at 06:17
  • Is there a way to directly monitor requests going out via an http client? So as to see how many requests are waiting for previous to complete before initiating? – Noah Eldreth Dec 08 '20 at 12:58
  • 1
    Since no one answered you, in regards to `Is there a way to directly monitor requests going out via an http client?`, you can derive from something like a `DelegatingHandler` or `HttpClientHandler` to inject your own code into the http request pipeline. Not saying you should or shouldn't, just putting it out there haha. The MaxConnectionsPerServer mentioned in the link Theodor posted may be better than writing your own solution depending on if it meets your needs.. – AotN Dec 10 '20 at 14:43
  • Appreciated. I ended up refactoring my code such that the expected responses are lighter, you query for groups individually as oppose to a bulk fetch, and simply used async tasks for the set of incidents and tasks. I tried a few other ways to reduce my wait time and it just wouldn't take. Also, what you're suggesting flies just a bit over my head in terms of implementation haha (unless you mean simply utilizing the handler interface and writing up a class to log requests and such). Makes sense that would work. Again thanks for the help! You're on point haha – Noah Eldreth Dec 10 '20 at 15:01

1 Answers1

1

Here's "a" solution. It allows you to await all the tasks in parallel while keeping the results bundled per group. You could, theoretically, if you wanted, move the logic for adding values to the dictionary/arraylist into the select itself if you choose to use concurrent collections instead.

Sorry if I did mess something up here. I tried to not over-assume anything. Intellisense also didn't work since you didn't include a couple of variables..

using (var client = ClientFactory.GetClientAsync(_apiCredentials))
{
    var results = await Task.WhenAll(
        groups.CurrentPage
            .Select(async group => {
                try
                {
                    var details = await _graphServiceClient.Groups[group.Id].Request().GetAsync();
                    var name = details.DisplayName.Replace("#", "").Trim();
                    // fetching Incidents and Tasks
                    var incidentsTask = GetIncidents(client, startDate, name);
                    var tasksTask = GetTasks(client, startDate, name);
                    await Task.WhenAll(incidentsTask, tasksTask);
                    return new { name, incidents = incidentsTask.Result, tasks = tasksTask.Result };
                }
                catch (Exception e)
                {
                    _logger.LogError(e.Message);
                    return null;
                }
            }));
    foreach (var result in results.Where(x => x != null))
    {
        //if both Incidents and Tasks exist for current group
        if (result.incidents != null && result.tasks != null)
        {
            final.Add(result.name, new ArrayList { 
                result.incidents,
                result.tasks
            });
        }
    }
}
AotN
  • 548
  • 2
  • 11
  • Thank you first off! I want to confirm if this code is or isn't still awaiting results from GetIncidents and GetTasks with each select from the list of groups. I could easily be wrong but those await statements would halt the thread so it would only continue to add tasks once the two current have completed.?? (I implemented this into my controller to test, and the response time from my query to this controller is still the same relatively. – Noah Eldreth Dec 07 '20 at 18:47
  • I looked at it again and realized I can store the task rather than the result under comment: "fetching Incidents and Tasks", then await them in the foreach loop. – Noah Eldreth Dec 07 '20 at 19:24
  • @NoahEldreth, you're right. I forgot to spawn them off onto separate threads with Task.Run. I've corrected it. I would not await them in your foreach loop as some of the tasks may not be complete and the foreach loop may end up having to wait for a few of them, resulting in theoretically less than full parallel processing. Let me know if the above fix helps at all! – AotN Dec 07 '20 at 20:57
  • Unfortunately, this isn't reducing wait time for me. It also occasionally fails which is odd. Running a test hardcoded query takes about 7 - 8s to process and return. Given my user the summed latency matches about what the number of groups * 7.5s would be. So, I'm not sure what other options I have outside of asynchronous programming. This is going straight to the API Controller endpoint - not even a View controller that would also have html, css, and js to process. – Noah Eldreth Dec 07 '20 at 22:12
  • Ah, sorry about my own confusion on this. I was trying to parallelize your requests _per_ group. So, because of that, you wouldn't have seen any gains unless running requests for multiple groups. In the case of what it sounds like you're actually asking, then I think Knoop's advice is best here. I've added it into the code above as well. I've taken back out Task.Run since it doesn't sound like there was really a problem with that portion and it's generally used for CPU bound operations. Again, apologies on my part. Let me know if this runs any better... – AotN Dec 08 '20 at 04:11
  • Also, the additional parallelized processing of groups in this way may not be what you want at all, in which case you may want to revert my outer Task.WhenAll/Select and just narrow down the code changes to what Knoops originally recommended using result/await after the inner Task.WhenAll.. – AotN Dec 08 '20 at 05:01
  • I think you're right. Also thank you again for all your help. Watching each line execute concurrently I think there is runtime errors occurring between threads. I can see in my GetIncidents( ) function for example, a return for a group that has incidents/tasks assigned seemingly gets disposed of unintentionally and returns null. What you suggested might be the way to go – Noah Eldreth Dec 08 '20 at 13:03
  • No problem, and thanks for being polite regarding my confusion lol. Yea, with the limited scope we've been provided, I wouldn't feel too comfortable right now trying to solve inner concurrency problems. Hope things go well though. – AotN Dec 08 '20 at 14:33