using Health.Application.Reports; using Health.Infrastructure.Data.Records; namespace Health.Infrastructure.Reports; public sealed class EfReportAnalysisQueue(AppDbContext db) : IReportAnalysisQueue { private const int MaxAttempts = 3; private readonly AppDbContext _db = db; public async Task EnqueueAsync(ReportAnalysisJob job, CancellationToken ct = default) { var now = DateTime.UtcNow; await _db.ReportAnalysisTasks.AddAsync(new ReportAnalysisTaskRecord { Id = job.TaskId, ReportId = job.ReportId, FilePath = job.FilePath, Status = "Pending", Attempts = 0, AvailableAt = now, CreatedAt = now, UpdatedAt = now, }, ct); await _db.SaveChangesAsync(ct); } public Task RecoverStaleAsync(CancellationToken ct) { var now = DateTime.UtcNow; return _db.ReportAnalysisTasks .Where(x => x.Status == "Processing" && x.UpdatedAt < now.AddMinutes(-5) && x.Attempts < MaxAttempts) .ExecuteUpdateAsync(setters => setters .SetProperty(x => x.Status, "Pending") .SetProperty(x => x.AvailableAt, now) .SetProperty(x => x.UpdatedAt, now), ct); } public async Task TryTakeAsync(CancellationToken ct) { var now = DateTime.UtcNow; var candidate = await _db.ReportAnalysisTasks.AsNoTracking() .Where(x => x.Status == "Pending" && x.AvailableAt <= now && x.Attempts < MaxAttempts) .OrderBy(x => x.CreatedAt) .FirstOrDefaultAsync(ct); if (candidate == null) return null; var claimed = await _db.ReportAnalysisTasks .Where(x => x.Id == candidate.Id && x.Status == "Pending") .ExecuteUpdateAsync(setters => setters .SetProperty(x => x.Status, "Processing") .SetProperty(x => x.Attempts, x => x.Attempts + 1) .SetProperty(x => x.UpdatedAt, now), ct); return claimed == 0 ? null : new ReportAnalysisJob(candidate.Id, candidate.ReportId, candidate.FilePath); } public async Task CompleteAsync(Guid taskId, CancellationToken ct) { await _db.ReportAnalysisTasks .Where(x => x.Id == taskId && x.Status == "Processing") .ExecuteUpdateAsync(setters => setters .SetProperty(x => x.Status, "Completed") .SetProperty(x => x.UpdatedAt, DateTime.UtcNow), ct); } public async Task RetryAsync(Guid taskId, string error, CancellationToken ct) { var task = await _db.ReportAnalysisTasks.FirstOrDefaultAsync(x => x.Id == taskId, ct); if (task == null) return; task.LastError = error.Length > 2000 ? error[..2000] : error; task.UpdatedAt = DateTime.UtcNow; if (task.Attempts >= MaxAttempts) { task.Status = "Failed"; } else { task.Status = "Pending"; task.AvailableAt = DateTime.UtcNow.AddSeconds(Math.Pow(2, task.Attempts) * 5); } await _db.SaveChangesAsync(ct); } }