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); } } } }