Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| feb95f5621 | |||
| 3c3c7caa27 |
@@ -2,6 +2,7 @@ using System.Text.Json;
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using FluentResults;
|
using FluentResults;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Tnsu.ApprovalRoute.Application.Common.Errors;
|
using Tnsu.ApprovalRoute.Application.Common.Errors;
|
||||||
using Tnsu.ApprovalRoute.Application.ListPartitioning;
|
using Tnsu.ApprovalRoute.Application.ListPartitioning;
|
||||||
using Tnsu.ApprovalRoute.Domain.Entities;
|
using Tnsu.ApprovalRoute.Domain.Entities;
|
||||||
@@ -25,10 +26,16 @@ namespace Tnsu.ApprovalRoute.Application.RequestManage;
|
|||||||
public sealed class RequestManageService(
|
public sealed class RequestManageService(
|
||||||
ApprovalRouteDbContext db,
|
ApprovalRouteDbContext db,
|
||||||
ISharePointClient sharePoint,
|
ISharePointClient sharePoint,
|
||||||
ListPartitionRegistry partitionRegistry)
|
ListPartitionRegistry partitionRegistry,
|
||||||
|
ILogger<RequestManageService> logger)
|
||||||
{
|
{
|
||||||
private const int DefaultPageSize = 20;
|
private const int DefaultPageSize = 20;
|
||||||
|
|
||||||
|
// Сколько типов заявок читаем из SharePoint одновременно. Без ограничения
|
||||||
|
// 18 типов × партиции разом упирались в троттлинг (429), а ошибки глотались
|
||||||
|
// и экран показывал пустой список.
|
||||||
|
private const int MaxParallelTypes = 3;
|
||||||
|
|
||||||
// Внутренние имена колонок списка согласования — 1:1 с FIELDS в
|
// Внутренние имена колонок списка согласования — 1:1 с FIELDS в
|
||||||
// requestManageService.ts (тот же самый список SharePoint, тот же провижининг).
|
// requestManageService.ts (тот же самый список SharePoint, тот же провижининг).
|
||||||
private const string ApproverField = "TansuSiteApprover";
|
private const string ApproverField = "TansuSiteApprover";
|
||||||
@@ -145,32 +152,51 @@ public sealed class RequestManageService(
|
|||||||
var statusFilter = string.Join(" or ", activeStatuses.Select(s => $"{StatusField} eq '{Escape(s)}'"));
|
var statusFilter = string.Join(" or ", activeStatuses.Select(s => $"{StatusField} eq '{Escape(s)}'"));
|
||||||
var email = approverEmail.Trim();
|
var email = approverEmail.Trim();
|
||||||
|
|
||||||
var results = await Task.WhenAll(
|
// DbContext не потокобезопасен: архивные партиции читаем из БД ПОСЛЕДОВАТЕЛЬНО,
|
||||||
requestTypes.Select(rt => FindForTypeSafeAsync(rt, statusFilter, email, cancellationToken)));
|
// и только дальнейшее чтение SharePoint (без БД) идёт параллельно. Раньше
|
||||||
|
// GetArchivedAsync вызывался из параллельных задач — «вторая операция на
|
||||||
|
// контексте» падала, а пустой catch превращал это в пустой список.
|
||||||
|
var archivedByType = new Dictionary<int, IReadOnlyList<RequestListPartition>>();
|
||||||
|
foreach (var rt in requestTypes)
|
||||||
|
archivedByType[rt.Id] = await partitionRegistry.GetArchivedAsync(rt, cancellationToken);
|
||||||
|
|
||||||
|
using var gate = new SemaphoreSlim(MaxParallelTypes);
|
||||||
|
var results = await Task.WhenAll(requestTypes.Select(async rt =>
|
||||||
|
{
|
||||||
|
await gate.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await FindForTypeSafeAsync(rt, archivedByType[rt.Id], statusFilter, email, cancellationToken);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
gate.Release();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
return Result.Ok<IReadOnlyList<UserApprovalRowDto>>(results.SelectMany(r => r).ToList());
|
return Result.Ok<IReadOnlyList<UserApprovalRowDto>>(results.SelectMany(r => r).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IReadOnlyList<UserApprovalRowDto>> FindForTypeSafeAsync(
|
private async Task<IReadOnlyList<UserApprovalRowDto>> FindForTypeSafeAsync(
|
||||||
RequestType requestType, string statusFilter, string approverEmail, CancellationToken cancellationToken)
|
RequestType requestType, IReadOnlyList<RequestListPartition> archived, string statusFilter, string approverEmail, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await FindForTypeAsync(requestType, statusFilter, approverEmail, cancellationToken);
|
return await FindForTypeAsync(requestType, archived, statusFilter, approverEmail, cancellationToken);
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
{
|
{
|
||||||
// Один недоступный/переименованный список не должен обнулять результат по
|
// Один недоступный/переименованный список не должен обнулять результат по
|
||||||
// остальным типам заявок — та же терпимость, что и в ReminderService.
|
// остальным типам заявок — та же терпимость, что и в ReminderService.
|
||||||
|
logger.LogWarning(ex, "by-approver: тип заявок {RequestTypeId} «{RequestTypeName}» пропущен", requestType.Id, requestType.Name);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IReadOnlyList<UserApprovalRowDto>> FindForTypeAsync(
|
private async Task<IReadOnlyList<UserApprovalRowDto>> FindForTypeAsync(
|
||||||
RequestType requestType, string statusFilter, string approverEmail, CancellationToken cancellationToken)
|
RequestType requestType, IReadOnlyList<RequestListPartition> archived, string statusFilter, string approverEmail, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var siteUrl = requestType.SiteUrl!;
|
var siteUrl = requestType.SiteUrl!;
|
||||||
var archived = await partitionRegistry.GetArchivedAsync(requestType, cancellationToken);
|
|
||||||
var current = await partitionRegistry.BuildCurrentAsync(requestType, archived, cancellationToken);
|
var current = await partitionRegistry.BuildCurrentAsync(requestType, archived, cancellationToken);
|
||||||
|
|
||||||
var partitions = archived
|
var partitions = archived
|
||||||
@@ -190,9 +216,11 @@ public sealed class RequestManageService(
|
|||||||
hits = await sharePoint.GetItemsByListTitleAsync(
|
hits = await sharePoint.GetItemsByListTitleAsync(
|
||||||
siteUrl, approvalListTitle, $"{ApprovalRowSelect}&$filter=({statusFilter})&$top=2000", cancellationToken);
|
siteUrl, approvalListTitle, $"{ApprovalRowSelect}&$filter=({statusFilter})&$top=2000", cancellationToken);
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
{
|
{
|
||||||
continue; // партиция сломана/переименована вручную — остальные не теряем.
|
// партиция сломана/переименована вручную — остальные не теряем.
|
||||||
|
logger.LogWarning(ex, "by-approver: список «{ApprovalList}» пропущен", approvalListTitle);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var matched = hits
|
var matched = hits
|
||||||
|
|||||||
Reference in New Issue
Block a user