using Microsoft.Extensions.Options;
using Workflow.Application.BuildFlow.Imports.Services;
using Workflow.Application.Notifications;
using Workflow.Domain.Enums;
using Workflow.Infrastructure.Persistence;
namespace BuildFlow.Host.Services.BackgroundServices;
///
/// 导入任务后台处理器:周期性扫描 Pending 任务,置为 Running 后调用 ImportJobProcessor。
/// 参考 TimeoutSchedulerService 结构:BackgroundService 单例 + IServiceScopeFactory 创建独立 scope,
/// 以便解析 scoped 依赖(WorkflowDbContext、IImportJobProcessor)。
///
public class ImportJobProcessorService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger _logger;
private readonly IOptionsMonitor _options;
public ImportJobProcessorService(
IServiceScopeFactory scopeFactory,
ILogger logger,
IOptionsMonitor options)
{
_scopeFactory = scopeFactory;
_logger = logger;
_options = options;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// timer 间隔启动时定一次(方案 A:timer 参数不热重载,改间隔需重启服务)
var opts = _options.CurrentValue;
_logger.LogInformation("ImportJobProcessor 启动,轮询间隔 {Interval}s", opts.Scheduler.PollIntervalSeconds);
// 启动时先等待一轮,避免与应用启动并发争抢资源
await Task.Delay(opts.Scheduler.StartupDelaySeconds * 1000, stoppingToken);
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(opts.Scheduler.PollIntervalSeconds));
// 整个 do-while 纳入 try/catch:timer.WaitForNextTickAsync 在 stoppingToken 取消时
// 会抛 OperationCanceledException,该异常发生在 while 条件求值处(不在内层 try 范围),
// 若不捕获会逃逸出 ExecuteAsync,触发 BackgroundServiceExceptionBehavior=StopHost 导致进程崩溃。
try
{
do
{
try
{
await RunScanAsync(stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "ImportJobProcessor 单轮处理异常");
}
}
while (await timer.WaitForNextTickAsync(stoppingToken));
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// 正常停机:WaitForNextTickAsync 被取消,优雅退出
}
_logger.LogInformation("ImportJobProcessor 停止");
}
/// 执行一轮扫描:拉取 Pending 任务 ID 列表,逐个调用 IImportJobProcessor.ProcessAsync。
public async Task RunScanAsync(CancellationToken ct = default)
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
var processor = scope.ServiceProvider.GetRequiredService();
var pendingIds = await db.BuildFlowImportJobs
.AsNoTracking()
.Where(j => j.Status == BuildFlowImportJobStatus.Pending)
.OrderBy(j => j.CreatedAt)
.Select(j => j.Id)
.Take(20)
.ToListAsync(ct);
foreach (var jobId in pendingIds)
{
ct.ThrowIfCancellationRequested();
try
{
await processor.ProcessAsync(jobId, ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "导入任务 {JobId} 处理失败", jobId);
}
}
}
}