Fix/by approver silent failures #25

Merged
gorbenko.pavel merged 6 commits from fix/by-approver-silent-failures into main 2026-10-05 04:38:37 +00:00
Showing only changes of commit 3c3c7caa27 - Show all commits
@@ -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,8 +152,19 @@ 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( using var gate = new SemaphoreSlim(MaxParallelTypes);
requestTypes.Select(rt => FindForTypeSafeAsync(rt, statusFilter, email, cancellationToken))); var results = await Task.WhenAll(requestTypes.Select(async rt =>
{
await gate.WaitAsync(cancellationToken);
try
{
return await FindForTypeSafeAsync(rt, 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());
} }
@@ -158,10 +176,11 @@ public sealed class RequestManageService(
{ {
return await FindForTypeAsync(requestType, statusFilter, approverEmail, cancellationToken); return await FindForTypeAsync(requestType, 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 [];
} }
} }
@@ -190,9 +209,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