Убрать неиспользуемые using по код-стайлу
Прогон dotnet format (IDE0005) по 4 решениям: удалены лишние using, оставшиеся после миграции namespace (676 файлов).
This commit is contained in:
@@ -1,196 +1,187 @@
|
||||
using Deal.Modules.Discovery.Application.Abstractions;
|
||||
using Deal.Modules.Discovery.Application.Exceptions;
|
||||
using Deal.Modules.Discovery.Application.Extensions;
|
||||
using Deal.Modules.Discovery.Application.Models;
|
||||
using Deal.Modules.Discovery.Application.Registrars;
|
||||
using Deal.Modules.Discovery.Application.Services;
|
||||
using Deal.Modules.Tenants.Application.Abstractions;
|
||||
using Deal.Modules.Tenants.Application.Extensions;
|
||||
using Deal.Modules.Tenants.Application.Models;
|
||||
using Deal.Modules.Tenants.Application.Registrars;
|
||||
using Deal.Modules.Tenants.Application.Services;
|
||||
using Deal.SharedKernel.Tenants.Abstractions;
|
||||
using Deal.SharedKernel.Tenants.Models;
|
||||
using Deal.Api.Services;
|
||||
using Deal.Api.Dtos;
|
||||
|
||||
namespace Deal.Api.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Фоновый цикл Discovery-воркера по всем тенантам (план Task 18, Ruling 10; эталон PipelineWorkerScheduler/StorageTickScheduler).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Каждые 5 с (в прототипе — <c>_discovery_loop</c> main.py: tick раз в ~5 секунд) обходит ВСЕ тенанты
|
||||
/// системного реестра и для каждого выполняет один тик <see cref="DiscoveryWorkerService.TickOnceAsync"/> в
|
||||
/// собственном scope с <c>ITenantContext.SetTenant</c> (эталон StorageTickScheduler). Тик делает ОДНО действие
|
||||
/// (поиск/оценка/вступление/done) для самой старой running-задачи тенанта — 1:1 с discovery_worker.tick L444–484.
|
||||
/// Как StorageTickScheduler: первый проход — сразу после старта, далее по таймеру; перекрывающиеся проходы
|
||||
/// исключены in-flight guard (Interlocked) — следующее срабатывание пропускается, если проход длится дольше
|
||||
/// периода (в частности, при паузах авто-вступлений 50–70 с внутри тика). Ошибки логируются и наружу не
|
||||
/// выбрасываются (тик одного тенанта не валит проход); при остановке хоста таймер останавливается и текущий
|
||||
/// проход отменяется (graceful). Пустой проход (нет running-задач/пауза/flood) — тихий no-op (action none).
|
||||
/// </remarks>
|
||||
public sealed class DiscoveryWorkerScheduler : IHostedService
|
||||
{
|
||||
// Период тиков цикла — 5 с (в прототипе _discovery_loop: tick каждые ~5 секунд).
|
||||
private const int DiscoveryPeriodSeconds = 5;
|
||||
|
||||
private static readonly TimeSpan DiscoveryPeriod = TimeSpan.FromSeconds(DiscoveryPeriodSeconds);
|
||||
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<DiscoveryWorkerScheduler> _logger;
|
||||
|
||||
// Отмена при остановке хоста: прерывает текущий проход (EF-запросы тика наблюдают токен).
|
||||
private readonly CancellationTokenSource _shutdownCts = new();
|
||||
|
||||
private Timer? _timer;
|
||||
private Task? _currentIteration;
|
||||
private int _iterationInProgress;
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт планировщик фонового цикла Discovery.
|
||||
/// </summary>
|
||||
/// <param name="scopeFactory">Фабрика scope: проход цикла и тик каждого тенанта — в собственных scope.</param>
|
||||
/// <param name="logger">Логгер ошибок цикла.</param>
|
||||
public DiscoveryWorkerScheduler(IServiceScopeFactory scopeFactory, ILogger<DiscoveryWorkerScheduler> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scopeFactory);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken ct)
|
||||
{
|
||||
// Первый проход — сразу после старта (как StorageTickScheduler), далее каждые 5 с.
|
||||
_timer = new Timer(
|
||||
static state => ((DiscoveryWorkerScheduler)state!).RunIteration(),
|
||||
this,
|
||||
TimeSpan.Zero,
|
||||
DiscoveryPeriod);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StopAsync(CancellationToken ct)
|
||||
{
|
||||
// Новые проходы не запускаем; текущий отменяем и ждём его завершения — не дольше лимита
|
||||
// остановки хоста (HostOptions.ShutdownTimeout).
|
||||
_timer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
|
||||
_shutdownCts.Cancel();
|
||||
Task? iteration = Volatile.Read(ref _currentIteration);
|
||||
if (iteration is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await iteration.WaitAsync(ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Лимит остановки истёк — хост продолжает остановку; ошибки прохода проглочены внутри
|
||||
// RunCycleAsync, незавершённый проход безопасно завершится на отменённом токене.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Один проход цикла: список тенантов реестра и тик каждого (no-op, если проход уже идёт).
|
||||
/// </summary>
|
||||
/// <remarks>Публичен как точка запуска прохода для unit-тестов (тайминги цикла не тестируются) и
|
||||
/// ручного вызова при отладке; таймер вызывает этот же метод. Ошибки и отмена токена наружу не
|
||||
/// выбрасываются: сбои логируются (цикл живёт), отмена по токену останова завершает проход штатно.</remarks>
|
||||
/// <param name="ct">Токен отмены прохода (в проде — токен остановки хоста).</param>
|
||||
/// <returns>Задача прохода (завершается без исключений).</returns>
|
||||
public Task RunCycleAsync(CancellationToken ct)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _iterationInProgress, 1, 0) != 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task iteration = RunCycleCoreAsync(ct);
|
||||
Volatile.Write(ref _currentIteration, iteration);
|
||||
return iteration;
|
||||
}
|
||||
|
||||
// Запускает проход из callback таймера (guard — внутри RunCycleAsync).
|
||||
private void RunIteration()
|
||||
{
|
||||
_ = RunCycleAsync(_shutdownCts.Token);
|
||||
}
|
||||
|
||||
// Тело прохода: тик Discovery каждого тенанта реестра; guard сбрасывается в finally.
|
||||
// ct: Токен отмены (остановка хоста).
|
||||
private async Task RunCycleCoreAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using AsyncServiceScope cycleScope = _scopeFactory.CreateAsyncScope();
|
||||
ITenantRepository tenantRepository = cycleScope.ServiceProvider.GetRequiredService<ITenantRepository>();
|
||||
IReadOnlyList<TenantRecordDto> tenants = await tenantRepository.ListAsync(ct);
|
||||
|
||||
foreach (TenantRecordDto tenant in tenants)
|
||||
{
|
||||
await TickTenantAsync(tenant, ct);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Остановка хоста: проход прерван по токену — штатный выход, не ошибка.
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// Сбой всего прохода (реестр недоступен и т.п.): логируем, цикл продолжит со следующего тика.
|
||||
_logger.LogError(exception, "Цикл Discovery: проход по тенантам не удался");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _iterationInProgress, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Тик одного тенанта в собственном scope: SetTenant → один тик воркера; Reset в finally.
|
||||
// Контекст AsyncLocal сбрасывается в finally, чтобы не переживать scope тенанта (как
|
||||
// SessionMiddleware). Тик одного тенанта не валит проход: ошибка логируется, остальные тенанты
|
||||
// обрабатываются; отмена (OCE) пробрасывается наверх — проход завершается. Результат «none» — тихий
|
||||
// no-op (пауза/нет работы); прочие действия логируются в Debug (рабочие события — в DiscLog задачи).
|
||||
// tenant: Тенант реестра (Id в формате Guid; схема — tenant_<N>).
|
||||
// ct: Токен отмены прохода.
|
||||
private async Task TickTenantAsync(TenantRecordDto tenant, CancellationToken ct)
|
||||
{
|
||||
await using AsyncServiceScope tenantScope = _scopeFactory.CreateAsyncScope();
|
||||
ITenantContext tenantContext = tenantScope.ServiceProvider.GetRequiredService<ITenantContext>();
|
||||
try
|
||||
{
|
||||
tenantContext.SetTenant(new TenantId(tenant.Id.ToString("N")));
|
||||
|
||||
// Resolve ПОСЛЕ SetTenant: TenantDbContext (и его адаптеры) строятся от схемы текущего тенанта.
|
||||
DiscoveryWorkerService worker = tenantScope.ServiceProvider.GetRequiredService<DiscoveryWorkerService>();
|
||||
DiscoveryWorkerOutcome outcome = await worker.TickOnceAsync(ct);
|
||||
if (outcome.Action != DiscoveryWorkerService.ActionNone)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Discovery-тик {TenantId}: {Action} (задача {TaskId})",
|
||||
tenant.Id,
|
||||
outcome.Action,
|
||||
outcome.TaskId ?? "-");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "Цикл Discovery: тик тенанта {TenantId} не удался", tenant.Id);
|
||||
}
|
||||
finally
|
||||
{
|
||||
tenantContext.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
using Deal.Modules.Discovery.Application.Models;
|
||||
using Deal.Modules.Discovery.Application.Services;
|
||||
using Deal.Modules.Tenants.Application.Abstractions;
|
||||
using Deal.Modules.Tenants.Application.Models;
|
||||
using Deal.SharedKernel.Tenants.Abstractions;
|
||||
using Deal.SharedKernel.Tenants.Models;
|
||||
|
||||
namespace Deal.Api.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Фоновый цикл Discovery-воркера по всем тенантам (план Task 18, Ruling 10; эталон PipelineWorkerScheduler/StorageTickScheduler).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Каждые 5 с (в прототипе — <c>_discovery_loop</c> main.py: tick раз в ~5 секунд) обходит ВСЕ тенанты
|
||||
/// системного реестра и для каждого выполняет один тик <see cref="DiscoveryWorkerService.TickOnceAsync"/> в
|
||||
/// собственном scope с <c>ITenantContext.SetTenant</c> (эталон StorageTickScheduler). Тик делает ОДНО действие
|
||||
/// (поиск/оценка/вступление/done) для самой старой running-задачи тенанта — 1:1 с discovery_worker.tick L444–484.
|
||||
/// Как StorageTickScheduler: первый проход — сразу после старта, далее по таймеру; перекрывающиеся проходы
|
||||
/// исключены in-flight guard (Interlocked) — следующее срабатывание пропускается, если проход длится дольше
|
||||
/// периода (в частности, при паузах авто-вступлений 50–70 с внутри тика). Ошибки логируются и наружу не
|
||||
/// выбрасываются (тик одного тенанта не валит проход); при остановке хоста таймер останавливается и текущий
|
||||
/// проход отменяется (graceful). Пустой проход (нет running-задач/пауза/flood) — тихий no-op (action none).
|
||||
/// </remarks>
|
||||
public sealed class DiscoveryWorkerScheduler : IHostedService
|
||||
{
|
||||
// Период тиков цикла — 5 с (в прототипе _discovery_loop: tick каждые ~5 секунд).
|
||||
private const int DiscoveryPeriodSeconds = 5;
|
||||
|
||||
private static readonly TimeSpan DiscoveryPeriod = TimeSpan.FromSeconds(DiscoveryPeriodSeconds);
|
||||
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<DiscoveryWorkerScheduler> _logger;
|
||||
|
||||
// Отмена при остановке хоста: прерывает текущий проход (EF-запросы тика наблюдают токен).
|
||||
private readonly CancellationTokenSource _shutdownCts = new();
|
||||
|
||||
private Timer? _timer;
|
||||
private Task? _currentIteration;
|
||||
private int _iterationInProgress;
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт планировщик фонового цикла Discovery.
|
||||
/// </summary>
|
||||
/// <param name="scopeFactory">Фабрика scope: проход цикла и тик каждого тенанта — в собственных scope.</param>
|
||||
/// <param name="logger">Логгер ошибок цикла.</param>
|
||||
public DiscoveryWorkerScheduler(IServiceScopeFactory scopeFactory, ILogger<DiscoveryWorkerScheduler> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scopeFactory);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken ct)
|
||||
{
|
||||
// Первый проход — сразу после старта (как StorageTickScheduler), далее каждые 5 с.
|
||||
_timer = new Timer(
|
||||
static state => ((DiscoveryWorkerScheduler)state!).RunIteration(),
|
||||
this,
|
||||
TimeSpan.Zero,
|
||||
DiscoveryPeriod);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StopAsync(CancellationToken ct)
|
||||
{
|
||||
// Новые проходы не запускаем; текущий отменяем и ждём его завершения — не дольше лимита
|
||||
// остановки хоста (HostOptions.ShutdownTimeout).
|
||||
_timer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
|
||||
_shutdownCts.Cancel();
|
||||
Task? iteration = Volatile.Read(ref _currentIteration);
|
||||
if (iteration is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await iteration.WaitAsync(ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Лимит остановки истёк — хост продолжает остановку; ошибки прохода проглочены внутри
|
||||
// RunCycleAsync, незавершённый проход безопасно завершится на отменённом токене.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Один проход цикла: список тенантов реестра и тик каждого (no-op, если проход уже идёт).
|
||||
/// </summary>
|
||||
/// <remarks>Публичен как точка запуска прохода для unit-тестов (тайминги цикла не тестируются) и
|
||||
/// ручного вызова при отладке; таймер вызывает этот же метод. Ошибки и отмена токена наружу не
|
||||
/// выбрасываются: сбои логируются (цикл живёт), отмена по токену останова завершает проход штатно.</remarks>
|
||||
/// <param name="ct">Токен отмены прохода (в проде — токен остановки хоста).</param>
|
||||
/// <returns>Задача прохода (завершается без исключений).</returns>
|
||||
public Task RunCycleAsync(CancellationToken ct)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _iterationInProgress, 1, 0) != 0)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task iteration = RunCycleCoreAsync(ct);
|
||||
Volatile.Write(ref _currentIteration, iteration);
|
||||
return iteration;
|
||||
}
|
||||
|
||||
// Запускает проход из callback таймера (guard — внутри RunCycleAsync).
|
||||
private void RunIteration()
|
||||
{
|
||||
_ = RunCycleAsync(_shutdownCts.Token);
|
||||
}
|
||||
|
||||
// Тело прохода: тик Discovery каждого тенанта реестра; guard сбрасывается в finally.
|
||||
// ct: Токен отмены (остановка хоста).
|
||||
private async Task RunCycleCoreAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using AsyncServiceScope cycleScope = _scopeFactory.CreateAsyncScope();
|
||||
ITenantRepository tenantRepository = cycleScope.ServiceProvider.GetRequiredService<ITenantRepository>();
|
||||
IReadOnlyList<TenantRecordDto> tenants = await tenantRepository.ListAsync(ct);
|
||||
|
||||
foreach (TenantRecordDto tenant in tenants)
|
||||
{
|
||||
await TickTenantAsync(tenant, ct);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Остановка хоста: проход прерван по токену — штатный выход, не ошибка.
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// Сбой всего прохода (реестр недоступен и т.п.): логируем, цикл продолжит со следующего тика.
|
||||
_logger.LogError(exception, "Цикл Discovery: проход по тенантам не удался");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _iterationInProgress, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Тик одного тенанта в собственном scope: SetTenant → один тик воркера; Reset в finally.
|
||||
// Контекст AsyncLocal сбрасывается в finally, чтобы не переживать scope тенанта (как
|
||||
// SessionMiddleware). Тик одного тенанта не валит проход: ошибка логируется, остальные тенанты
|
||||
// обрабатываются; отмена (OCE) пробрасывается наверх — проход завершается. Результат «none» — тихий
|
||||
// no-op (пауза/нет работы); прочие действия логируются в Debug (рабочие события — в DiscLog задачи).
|
||||
// tenant: Тенант реестра (Id в формате Guid; схема — tenant_<N>).
|
||||
// ct: Токен отмены прохода.
|
||||
private async Task TickTenantAsync(TenantRecordDto tenant, CancellationToken ct)
|
||||
{
|
||||
await using AsyncServiceScope tenantScope = _scopeFactory.CreateAsyncScope();
|
||||
ITenantContext tenantContext = tenantScope.ServiceProvider.GetRequiredService<ITenantContext>();
|
||||
try
|
||||
{
|
||||
tenantContext.SetTenant(new TenantId(tenant.Id.ToString("N")));
|
||||
|
||||
// Resolve ПОСЛЕ SetTenant: TenantDbContext (и его адаптеры) строятся от схемы текущего тенанта.
|
||||
DiscoveryWorkerService worker = tenantScope.ServiceProvider.GetRequiredService<DiscoveryWorkerService>();
|
||||
DiscoveryWorkerOutcome outcome = await worker.TickOnceAsync(ct);
|
||||
if (outcome.Action != DiscoveryWorkerService.ActionNone)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Discovery-тик {TenantId}: {Action} (задача {TaskId})",
|
||||
tenant.Id,
|
||||
outcome.Action,
|
||||
outcome.TaskId ?? "-");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "Цикл Discovery: тик тенанта {TenantId} не удался", tenant.Id);
|
||||
}
|
||||
finally
|
||||
{
|
||||
tenantContext.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user