optim(redmine): made most requests async

This commit is contained in:
2026-01-15 16:31:53 +01:00
parent 0b6df0c508
commit d152b62cc4
6 changed files with 185 additions and 72 deletions

View File

@@ -1,5 +1,7 @@
using Blueberry.Redmine.Dto;
using Microsoft.Extensions.Logging;
using System.ComponentModel.DataAnnotations;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -76,7 +78,7 @@ namespace Blueberry.Redmine
throw new RedmineApiException("Redmine API Unreachable");
}
/*
private async Task<List<TReturn>> SendRequestWithPagingAsync<TResponse, TReturn>(HttpMethod method, string endpoint, int limit, Func<TResponse, List<TReturn>> itemParser,
IProgress<(int current, int total)>? progress = null, object? payload = null, CancellationToken? token = null) where TResponse : IResponseList
{
@@ -84,10 +86,12 @@ namespace Blueberry.Redmine
List<TReturn> returnList = [];
_logger.LogDebug("Starting paged request to {Endpoint} with limit {Limit}", endpoint, limit);
while (true)
{
var path = "";
if(endpoint.Contains('?'))
if (endpoint.Contains('?'))
path = $"{endpoint}&limit={limit}&offset={offset}";
else
path = $"{endpoint}?limit={limit}&offset={offset}";
@@ -101,7 +105,7 @@ namespace Blueberry.Redmine
break;
offset += limit;
if(progress != null)
if (progress != null)
{
var current = Math.Min(offset + limit, responseList.TotalCount);
progress.Report((current, responseList.TotalCount));
@@ -110,6 +114,92 @@ namespace Blueberry.Redmine
return returnList;
}
*/
private async Task<List<TReturn>> SendRequestWithPagingAsync<TResponse, TReturn>(HttpMethod method, string endpoint, int limit, Func<TResponse, List<TReturn>> itemParser,
IProgress<(int current, int total)>? progress = null, object? payload = null, CancellationToken? token = null) where TResponse : IResponseList
{
var offset = 0;
List<TReturn> returnList = [];
_logger.LogDebug("Starting paged request to {Endpoint} with limit {Limit}", endpoint, limit);
var path = "";
if (endpoint.Contains('?'))
path = $"{endpoint}&limit={limit}&offset={offset}";
else
path = $"{endpoint}?limit={limit}&offset={offset}";
var responseList = await SendRequestAsync<TResponse>(HttpMethod.Get, path, token: token)
?? throw new NullReferenceException();
var total = responseList.TotalCount;
returnList.AddRange(itemParser(responseList));
offset += limit;
if (offset >= responseList.TotalCount)
return returnList;
var remain = total - offset;
var tasks = new Task[(int)Math.Ceiling((double)remain / limit)];
_logger.LogDebug("Spawning {TaskCount} tasks for remaining {Remaining} items out of {Total}", tasks.Length, remain, total);
List<(int id, IEnumerable<TReturn> items)> responses = [];
for (int i = 0; i < tasks.Length; i++)
{
var id = i;
var o = offset;
tasks[i] = Task.Run(async () =>
{
if (endpoint.Contains('?'))
path = $"{endpoint}&limit={limit}&offset={o}";
else
path = $"{endpoint}?limit={limit}&offset={o}";
var responseList = await SendRequestAsync<TResponse>(HttpMethod.Get, path, token: token)
?? throw new NullReferenceException();
responses.Add((id, itemParser(responseList)));
});
offset += limit;
}
while (tasks.Any(t => !t.IsCompleted))
{
if (progress != null)
{
var current = Math.Min(tasks.Count(x=>x.IsCompletedSuccessfully), tasks.Length);
progress.Report((current, tasks.Length));
}
var completed = tasks.Count(t => t.IsCompleted);
_logger.LogDebug("{Completed}/{Total} tasks completed", completed, tasks.Length);
await Task.Delay(250);
}
await Task.WhenAll(tasks);
await Task.Delay(100);
var notCompleted = tasks.Where(t => !t.IsCompletedSuccessfully).ToList();
_logger.LogDebug("{NotCompleted} tasks did not complete successfully", notCompleted.Count);
_logger.LogDebug("All tasks completed, aggregating {total} results from {count} responses", responses.Select(x=>x.items).Count(), responses.Count);
foreach (var resp in responses.OrderBy(x => x.id))
{
returnList.AddRange(resp.items);
}
_logger.LogDebug("Aggregated total of {TotalItems} items", returnList.Count);
return returnList;
}
public async Task<List<StatusList.IssueStatus>> GetStatusesAsync(CancellationToken? token = null)
{
@@ -364,7 +454,7 @@ namespace Blueberry.Redmine
{
var path = "users.json";
return await SendRequestWithPagingAsync<UserList.Root, UserInfo.User>(HttpMethod.Get, path, limit, (x) => x.Users, progress, token);
return [.. (await SendRequestWithPagingAsync<UserList.Root, UserInfo.User>(HttpMethod.Get, path, limit, (x) => x.Users, progress, token)).OrderBy(x => x.FullName)];
}
public async Task SetIssueStatusAsync(int issueId, int statusId, CancellationToken? token = null)