using Jiaowu.Api.Domain.System; using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; namespace Jiaowu.Api.Infrastructure.BackgroundJobs; public sealed record BackgroundJobBacklogSnapshot( int Pending, int Publishing, int Published, int Processing, int ExpiredLeases, DateTime? OldestUnfinishedAt, double? OldestUnfinishedAgeSeconds); public sealed class BackgroundJobMonitoringService(AppDbContext db) { public async Task GetSnapshotAsync( CancellationToken cancellationToken) { var counts = await db.BackgroundJobOutboxMessages.AsNoTracking() .Where(x => x.State != BackgroundJobOutboxState.Completed) .GroupBy(x => x.State) .Select(group => new { State = group.Key, Count = group.Count() }) .ToListAsync(cancellationToken); var countByState = counts.ToDictionary(x => x.State, x => x.Count); var now = DateTime.UtcNow; var expiredLeases = await db.BackgroundJobOutboxMessages.AsNoTracking() .CountAsync( x => (x.State == BackgroundJobOutboxState.Publishing || x.State == BackgroundJobOutboxState.Processing) && x.LeaseExpiresAt != null && x.LeaseExpiresAt < now, cancellationToken); var oldestUnfinishedAt = await db.BackgroundJobOutboxMessages .AsNoTracking() .Where(x => x.State != BackgroundJobOutboxState.Completed) .Select(x => (DateTime?)x.CreatedAt) .MinAsync(cancellationToken); double? ageSeconds = oldestUnfinishedAt.HasValue ? Math.Max(0, (now - oldestUnfinishedAt.Value).TotalSeconds) : null; return new BackgroundJobBacklogSnapshot( GetCount(BackgroundJobOutboxState.Pending), GetCount(BackgroundJobOutboxState.Publishing), GetCount(BackgroundJobOutboxState.Published), GetCount(BackgroundJobOutboxState.Processing), expiredLeases, oldestUnfinishedAt, ageSeconds); int GetCount(BackgroundJobOutboxState state) => countByState.GetValueOrDefault(state); } }