кэширование и расспаралеливание
This commit is contained in:
@@ -187,12 +187,18 @@ public sealed class TemplateRouteService(
|
|||||||
.OrderBy(s => s.OrderNo)
|
.OrderBy(s => s.OrderNo)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var steps = new List<ResolvedRouteStepDto>();
|
// Шаги резолвятся ПАРАЛЛЕЛЬНО, а не по очереди: каждый шаг — это отдельные
|
||||||
foreach (var step in matchingSteps)
|
// походы в api.tnsu.kz (см. ResolveStepAsync), и на длинном маршруте
|
||||||
steps.Add(await ResolveStepAsync(
|
// (десяток+ шагов) последовательные await один за другим складываются в
|
||||||
|
// заметную задержку. Task.WhenAll сохраняет порядок результатов (массив
|
||||||
|
// соответствует порядку задач, а не порядку завершения), поэтому
|
||||||
|
// OrderBy(OrderNo) выше и CollapseByExecutorEmail ниже (ей важно, ЧЕЙ
|
||||||
|
// дубликат считается первым) продолжают работать так же, как при
|
||||||
|
// последовательном foreach.
|
||||||
|
var steps = await Task.WhenAll(matchingSteps.Select(step => ResolveStepAsync(
|
||||||
step, email, employee.Company, projectExecutors, request.DepartmentId, requestType,
|
step, email, employee.Company, projectExecutors, request.DepartmentId, requestType,
|
||||||
HasUnevaluatableManagerCondition(step, positionMissing) ? PositionMissingWarning : null,
|
HasUnevaluatableManagerCondition(step, positionMissing) ? PositionMissingWarning : null,
|
||||||
cancellationToken));
|
cancellationToken)));
|
||||||
|
|
||||||
var collapsedSteps = CollapseByExecutorEmail(steps);
|
var collapsedSteps = CollapseByExecutorEmail(steps);
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -29,11 +29,17 @@ public static class InfrastructureServiceCollectionExtensions
|
|||||||
|
|
||||||
services.Configure<TnsuPortalOptions>(configuration.GetSection("TnsuPortal"));
|
services.Configure<TnsuPortalOptions>(configuration.GetSection("TnsuPortal"));
|
||||||
services.AddHttpClient<TnsuPortalTokenProvider>();
|
services.AddHttpClient<TnsuPortalTokenProvider>();
|
||||||
services.AddHttpClient<ITnsuPortalClient, HttpTnsuPortalClient>((sp, client) =>
|
// ITnsuPortalClient резолвится в CachingTnsuPortalClient (кеш на 5 минут,
|
||||||
|
// см. её комментарий) — сам HttpTnsuPortalClient регистрируем по
|
||||||
|
// конкретному типу, а не по интерфейсу, чтобы декоратор мог его получить
|
||||||
|
// как обычную зависимость.
|
||||||
|
services.AddHttpClient<HttpTnsuPortalClient>((sp, client) =>
|
||||||
{
|
{
|
||||||
var options = sp.GetRequiredService<IOptions<TnsuPortalOptions>>().Value;
|
var options = sp.GetRequiredService<IOptions<TnsuPortalOptions>>().Value;
|
||||||
client.BaseAddress = new Uri(options.BaseUrl);
|
client.BaseAddress = new Uri(options.BaseUrl);
|
||||||
});
|
});
|
||||||
|
services.AddMemoryCache();
|
||||||
|
services.AddScoped<ITnsuPortalClient, CachingTnsuPortalClient>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Кеширующая обёртка над <see cref="HttpTnsuPortalClient"/>. Резолв одного
|
||||||
|
/// маршрута (TemplateRouteService.ResolveAsync) может дёргать справочник
|
||||||
|
/// api.tnsu.kz по многу раз за один запрос — один и тот же согласующий,
|
||||||
|
/// должность или руководитель нередко повторяется в разных шагах длинного
|
||||||
|
/// маршрута, — и без кеша распараллеливание самих шагов резолва не спасает от
|
||||||
|
/// нагрузки на api.tnsu.kz: он просто получает то же число запросов
|
||||||
|
/// одновременно вместо по очереди. Кеш процессный (<see cref="IMemoryCache"/>,
|
||||||
|
/// не распределённый — реплик API несколько, поэтому в них он не общий), TTL
|
||||||
|
/// фиксирован на <see cref="CacheDuration"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CachingTnsuPortalClient(HttpTnsuPortalClient inner, IMemoryCache cache) : ITnsuPortalClient
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
|
public Task<EmployeeDirectoryInfo?> GetEmployeeByEmailAsync(string email, CancellationToken cancellationToken) =>
|
||||||
|
GetOrAddAsync(
|
||||||
|
$"emp:email:{email.ToLowerInvariant()}",
|
||||||
|
() => inner.GetEmployeeByEmailAsync(email, cancellationToken));
|
||||||
|
|
||||||
|
public Task<string?> GetEmployeeByPositionAsync(string position, string company, CancellationToken cancellationToken) =>
|
||||||
|
GetOrAddAsync(
|
||||||
|
$"emp:position:{position.ToLowerInvariant()}|{company.ToLowerInvariant()}",
|
||||||
|
() => inner.GetEmployeeByPositionAsync(position, company, cancellationToken));
|
||||||
|
|
||||||
|
public Task<string?> GetManagerAsync(string email, string managerType, CancellationToken cancellationToken) =>
|
||||||
|
GetOrAddAsync(
|
||||||
|
$"mgr:{email.ToLowerInvariant()}|{managerType.ToLowerInvariant()}",
|
||||||
|
() => inner.GetManagerAsync(email, managerType, cancellationToken));
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<ProjectExecutorInfo>> GetProjectExecutorsAsync(Guid projectId, CancellationToken cancellationToken) =>
|
||||||
|
GetOrAddAsync(
|
||||||
|
$"projexec:{projectId}",
|
||||||
|
() => inner.GetProjectExecutorsAsync(projectId, cancellationToken));
|
||||||
|
|
||||||
|
public Task<string?> GetDepartmentHeadEmailAsync(Guid departmentId, CancellationToken cancellationToken) =>
|
||||||
|
GetOrAddAsync(
|
||||||
|
$"depthead:{departmentId}",
|
||||||
|
() => inner.GetDepartmentHeadEmailAsync(departmentId, cancellationToken));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Кеширует и «не нашли» (null / пустой список) — повторять запрос к
|
||||||
|
/// api.tnsu.kz за тем же результатом на каждый одинаковый шаг маршрута
|
||||||
|
/// незачем. Ключ по одному и тому же значению может задеть гонку: два
|
||||||
|
/// конкурентных шага (см. Task.WhenAll в ResolveAsync) с одинаковым ключом
|
||||||
|
/// оба не найдут его в кеше и оба сходят в API — не страшно, это лишний
|
||||||
|
/// один поход, а не порча данных; блокировка per-key усложнила бы код ради
|
||||||
|
/// выигрыша, который при TTL в минуты и так почти всегда есть без неё.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<T> GetOrAddAsync<T>(string key, Func<Task<T>> factory)
|
||||||
|
{
|
||||||
|
if (cache.TryGetValue(key, out T? cached))
|
||||||
|
return cached!;
|
||||||
|
|
||||||
|
var value = await factory();
|
||||||
|
cache.Set(key, value, CacheDuration);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user