< Summary

Information
Class: OzonOrdersWeb.WebServices.JobLogs.JobLogsBadgeService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/JobLogs/JobLogsBadgeService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 197
Coverable lines: 197
Total lines: 308
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 78
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/JobLogs/JobLogsBadgeService.cs

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using Microsoft.Extensions.Caching.Memory;
 3using MySqlConnector;
 4using OzonDomains.Models;
 5using OzonRepositories.Context;
 6using Services.SearchServices.OrderSearch;
 7using OzonOrdersWeb.WebServices.SystemAlerts;
 8
 9namespace OzonOrdersWeb.WebServices.JobLogs;
 10
 11public class JobLogsBadgeService : IJobLogsBadgeService
 12{
 013    private static readonly TimeSpan RecentWindow = TimeSpan.FromHours(24);
 014    private static readonly TimeSpan ActiveRunningWindow = TimeSpan.FromHours(2);
 015    private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(15);
 016    private static readonly string[] TxFailureStatuses = ["Failed", "PartialSuccess"];
 017    private static readonly string[] BitrixFailureStatuses = ["Failed", "PartialSuccess"];
 18
 19    private readonly ApplicationDbContext _context;
 20    private readonly IMemoryCache _cache;
 21    private readonly IOrderSearchDiagnosticsService _orderSearchDiagnosticsService;
 22    private readonly SystemAlertFactory _systemAlertFactory;
 23
 024    public JobLogsBadgeService(
 025        ApplicationDbContext context,
 026        IMemoryCache cache,
 027        IOrderSearchDiagnosticsService orderSearchDiagnosticsService,
 028        SystemAlertFactory systemAlertFactory)
 29    {
 030        _systemAlertFactory = systemAlertFactory;
 031        _context = context;
 032        _cache = cache;
 033        _orderSearchDiagnosticsService = orderSearchDiagnosticsService;
 034    }
 35
 36    public Task<JobLogsBadgeSummaryDto> GetSummaryAsync(bool includeTransactions, CancellationToken cancellationToken = 
 37    {
 38        // Эндпоинт опрашивается фронтом на каждой странице (~раз в 30 с с каждой вкладки),
 39        // а внутри делает ~15 запросов. Кэшируем результат на короткое время,
 40        // чтобы всплеск опросов не нагружал пул соединений.
 041        var cacheKey = $"joblogs-badge-summary:{includeTransactions}";
 042        return _cache.GetOrCreateAsync(cacheKey, entry =>
 043        {
 044            entry.AbsoluteExpirationRelativeToNow = CacheTtl;
 045            return BuildSummaryAsync(includeTransactions, cancellationToken);
 046        })!;
 47    }
 48
 49    private async Task<JobLogsBadgeSummaryDto> BuildSummaryAsync(bool includeTransactions, CancellationToken cancellatio
 50    {
 051        var since = DateTime.UtcNow.Subtract(RecentWindow);
 052        var activeSince = DateTime.UtcNow.Subtract(ActiveRunningWindow);
 53
 054        return new JobLogsBadgeSummaryDto
 055        {
 056            Success = true,
 057            Clear = await BuildBitrixClearBadgeAsync(since, activeSince, cancellationToken),
 058            Push = await BuildBitrixPushBadgeAsync(since, activeSince, cancellationToken),
 059            Transactions = includeTransactions
 060                ? await BuildTransactionsBadgeAsync(since, activeSince, cancellationToken)
 061                : null,
 062            Webhook = await BuildWebhookBadgeAsync(since, cancellationToken),
 063            EmailRouting = await BuildEmailRoutingBadgeAsync(since, cancellationToken),
 064            FileUpload = await BuildFileUploadBadgeAsync(since, cancellationToken),
 065            StockSync = await BuildStockSyncBadgeAsync(since, activeSince, cancellationToken),
 066            OrderSearch = includeTransactions
 067                ? await BuildOrderSearchBadgeAsync(cancellationToken)
 068                : null,
 069            // Алерты отдаёт отдельный лёгкий эндпоинт alert-chips: сводка кэшируется и тяжёлая.
 070            SystemAlerts = []
 071        };
 072    }
 73
 74    /// <summary>Актуальные системные предупреждения из источников без кэша каналов.</summary>
 75    public Task<List<JobLogsSystemAlertDto>> GetSystemAlertsAsync(CancellationToken cancellationToken = default) =>
 076        _systemAlertFactory.GetAlertsAsync(cancellationToken);
 77
 78    private async Task<JobLogsOrderSearchBadgeDto> BuildOrderSearchBadgeAsync(CancellationToken cancellationToken)
 79    {
 80        try
 81        {
 082            var snapshot = await _orderSearchDiagnosticsService.GetSnapshotAsync(cancellationToken);
 083            return new JobLogsOrderSearchBadgeDto
 084            {
 085                HasCountDrift = snapshot.EsEnabled && snapshot.CountDrift > 0,
 086                CountDrift = snapshot.CountDrift
 087            };
 88        }
 089        catch
 90        {
 91            // Проблема диагностики ES не должна скрывать обычные бейджи журнала задач.
 092            return new JobLogsOrderSearchBadgeDto();
 93        }
 094    }
 95
 96    private async Task<JobLogsChannelBadgeDto> BuildBitrixClearBadgeAsync(
 97        DateTime since, DateTime activeSince, CancellationToken cancellationToken)
 98    {
 099        var latest = await _context.BitrixStockClearExecutions
 0100            .AsNoTracking()
 0101            .OrderByDescending(e => e.StartedAt)
 0102            .Select(e => new { e.Id, e.Status, e.StartedAt, e.CompletedAt })
 0103            .FirstOrDefaultAsync(cancellationToken);
 104
 0105        var activeRunningCount = await _context.BitrixStockClearExecutions
 0106            .AsNoTracking()
 0107            .CountAsync(e =>
 0108                e.Status == BitrixStockClearExecutionStatus.Running &&
 0109                e.StartedAt >= activeSince &&
 0110                e.CompletedAt == null, cancellationToken);
 111
 0112        var recentFailureCount = await _context.BitrixStockClearExecutions
 0113            .AsNoTracking()
 0114            .CountAsync(e => BitrixFailureStatuses.Contains(e.Status) && e.StartedAt >= since, cancellationToken);
 115
 0116        return MapChannel(latest?.Id, latest?.Status, latest?.StartedAt, latest?.CompletedAt,
 0117            activeRunningCount, recentFailureCount);
 0118    }
 119
 120    private async Task<JobLogsChannelBadgeDto> BuildBitrixPushBadgeAsync(
 121        DateTime since, DateTime activeSince, CancellationToken cancellationToken)
 122    {
 0123        var latest = await _context.BitrixStockPushExecutions
 0124            .AsNoTracking()
 0125            .OrderByDescending(e => e.StartedAt)
 0126            .Select(e => new { e.Id, e.Status, e.StartedAt, e.CompletedAt })
 0127            .FirstOrDefaultAsync(cancellationToken);
 128
 0129        var activeRunningCount = await _context.BitrixStockPushExecutions
 0130            .AsNoTracking()
 0131            .CountAsync(e =>
 0132                e.Status == BitrixStockPushExecutionStatus.Running &&
 0133                e.StartedAt >= activeSince &&
 0134                e.CompletedAt == null, cancellationToken);
 135
 0136        var recentFailureCount = await _context.BitrixStockPushExecutions
 0137            .AsNoTracking()
 0138            .CountAsync(e => BitrixFailureStatuses.Contains(e.Status) && e.StartedAt >= since, cancellationToken);
 139
 0140        return MapChannel(latest?.Id, latest?.Status, latest?.StartedAt, latest?.CompletedAt,
 0141            activeRunningCount, recentFailureCount);
 0142    }
 143
 144    private async Task<JobLogsChannelBadgeDto> BuildTransactionsBadgeAsync(
 145        DateTime since, DateTime activeSince, CancellationToken cancellationToken)
 146    {
 0147        var latest = await _context.BackgroundJobExecutions
 0148            .AsNoTracking()
 0149            .OrderByDescending(e => e.StartedAt)
 0150            .Select(e => new { e.Id, e.Status, e.StartedAt, e.CompletedAt })
 0151            .FirstOrDefaultAsync(cancellationToken);
 152
 0153        var activeRunningCount = await _context.BackgroundJobExecutions
 0154            .AsNoTracking()
 0155            .CountAsync(e =>
 0156                e.Status == "Running" &&
 0157                e.StartedAt >= activeSince &&
 0158                e.CompletedAt == null, cancellationToken);
 159
 0160        var recentFailureCount = await _context.BackgroundJobExecutions
 0161            .AsNoTracking()
 0162            .CountAsync(e => TxFailureStatuses.Contains(e.Status) && e.StartedAt >= since, cancellationToken);
 163
 0164        return MapChannel(latest?.Id, latest?.Status, latest?.StartedAt, latest?.CompletedAt,
 0165            activeRunningCount, recentFailureCount);
 0166    }
 167
 168    private async Task<JobLogsChannelBadgeDto> BuildWebhookBadgeAsync(DateTime since, CancellationToken cancellationToke
 169    {
 170        try
 171        {
 0172            var latest = await _context.OzonPushNotifications
 0173                .AsNoTracking()
 0174                .OrderByDescending(e => e.ReceivedAt)
 0175                .Select(e => new { e.Id, e.ProcessingStatus, e.ReceivedAt })
 0176                .FirstOrDefaultAsync(cancellationToken);
 177
 0178            var recentFailureCount = await _context.OzonPushNotifications
 0179                .AsNoTracking()
 0180                .CountAsync(e => e.ProcessingStatus == "Error" && e.ReceivedAt >= since, cancellationToken);
 181
 0182            var pendingCount = await _context.OzonPushNotifications
 0183                .AsNoTracking()
 0184                .CountAsync(e => e.ProcessingStatus == "Received" && e.ReceivedAt >= since, cancellationToken);
 185
 0186            var dto = MapChannel(latest?.Id, latest?.ProcessingStatus, latest?.ReceivedAt, null, 0, recentFailureCount);
 0187            dto.UnreadCount = recentFailureCount + pendingCount;
 0188            return dto;
 189        }
 0190        catch (MySqlException ex) when (ex.Message.Contains("doesn't exist", StringComparison.OrdinalIgnoreCase))
 191        {
 0192            return EmptyChannelBadge();
 193        }
 0194    }
 195
 196    private async Task<JobLogsChannelBadgeDto> BuildEmailRoutingBadgeAsync(DateTime since, CancellationToken cancellatio
 197    {
 0198        var activeSince = DateTime.UtcNow.Subtract(ActiveRunningWindow);
 0199        var latest = await _context.EmailRoutingJobExecutions
 0200            .AsNoTracking()
 0201            .OrderByDescending(e => e.StartedAtUtc)
 0202            .Select(e => new { e.Id, e.Status, e.StartedAtUtc, e.CompletedAtUtc })
 0203            .FirstOrDefaultAsync(cancellationToken);
 204
 0205        var activeRunningCount = await _context.EmailRoutingJobExecutions
 0206            .AsNoTracking()
 0207            .CountAsync(e =>
 0208                e.Status == EmailRoutingJobExecutionStatus.Running &&
 0209                e.StartedAtUtc >= activeSince &&
 0210                e.CompletedAtUtc == null, cancellationToken);
 211
 0212        var recentFailureCount = await _context.EmailRoutingJobExecutions
 0213            .AsNoTracking()
 0214            .CountAsync(e =>
 0215                e.Status == EmailRoutingJobExecutionStatus.Failed &&
 0216                e.StartedAtUtc >= since, cancellationToken);
 217
 0218        return MapChannel(
 0219            latest?.Id,
 0220            latest?.Status,
 0221            latest?.StartedAtUtc,
 0222            latest?.CompletedAtUtc,
 0223            activeRunningCount,
 0224            recentFailureCount);
 0225    }
 226
 227    private async Task<JobLogsChannelBadgeDto> BuildFileUploadBadgeAsync(DateTime since, CancellationToken cancellationT
 228    {
 0229        var latest = await _context.FileUploadRecords
 0230            .AsNoTracking()
 0231            .OrderByDescending(e => e.Date)
 0232            .Select(e => new { e.Id, e.Date, e.Result })
 0233            .FirstOrDefaultAsync(cancellationToken);
 234
 0235        var records = await _context.FileUploadRecords
 0236            .AsNoTracking()
 0237            .Where(e => e.Date >= since)
 0238            .Select(e => e.Result)
 0239            .ToListAsync(cancellationToken);
 240
 0241        var recentFailureCount = records.Count(r => !IsFileUploadSuccess(r));
 242
 0243        var status = latest == null
 0244            ? null
 0245            : IsFileUploadSuccess(latest.Result) ? "Success" : "Failed";
 246
 0247        return MapChannel(latest?.Id, status, latest?.Date, latest?.Date, 0, recentFailureCount);
 0248    }
 249
 250    private async Task<JobLogsChannelBadgeDto> BuildStockSyncBadgeAsync(
 251        DateTime since,
 252        DateTime activeSince,
 253        CancellationToken cancellationToken)
 254    {
 0255        var latest = await _context.WarehouseStockSyncRuns
 0256            .AsNoTracking()
 0257            .OrderByDescending(x => x.StartedAtUtc)
 0258            .Select(x => new { x.Id, x.Status, x.StartedAtUtc, x.CompletedAtUtc })
 0259            .FirstOrDefaultAsync(cancellationToken);
 260
 0261        var activeRunningCount = await _context.WarehouseStockSyncRuns
 0262            .AsNoTracking()
 0263            .CountAsync(x =>
 0264                x.Status == "Running" &&
 0265                x.StartedAtUtc >= activeSince &&
 0266                x.CompletedAtUtc == null, cancellationToken);
 267
 0268        var recentFailureCount = await _context.WarehouseStockSyncRuns
 0269            .AsNoTracking()
 0270            .CountAsync(x => x.Status == "Failed" && x.StartedAtUtc >= since, cancellationToken);
 271
 0272        return MapChannel(latest?.Id, latest?.Status, latest?.StartedAtUtc, latest?.CompletedAtUtc, activeRunningCount, 
 0273    }
 274
 275    private static bool IsFileUploadSuccess(string? result) =>
 0276        !string.IsNullOrWhiteSpace(result) &&
 0277        result.Contains("успешно", StringComparison.OrdinalIgnoreCase);
 278
 0279    private static JobLogsChannelBadgeDto EmptyChannelBadge() => new();
 280
 281    private static JobLogsChannelBadgeDto MapChannel(
 282        long? id,
 283        string? status,
 284        DateTime? startedAt,
 285        DateTime? completedAt,
 286        int activeRunningCount,
 287        int recentFailureCount)
 288    {
 0289        var latestAt = completedAt ?? startedAt;
 0290        var isActivelyRunning = activeRunningCount > 0;
 0291        var latestIsFailure = !string.IsNullOrEmpty(status) &&
 0292                              (TxFailureStatuses.Contains(status) || BitrixFailureStatuses.Contains(status) ||
 0293                               string.Equals(status, "Failed", StringComparison.OrdinalIgnoreCase) ||
 0294                               string.Equals(status, "Error", StringComparison.OrdinalIgnoreCase));
 295
 0296        return new JobLogsChannelBadgeDto
 0297        {
 0298            LatestId = id,
 0299            LatestAtUtc = latestAt,
 0300            LatestStatus = status,
 0301            IsRunning = isActivelyRunning,
 0302            HasRecentFailure = recentFailureCount > 0 || latestIsFailure,
 0303            RecentFailureCount = recentFailureCount,
 0304            RunningCount = activeRunningCount,
 0305            UnreadCount = recentFailureCount + (isActivelyRunning ? activeRunningCount : 0)
 0306        };
 307    }
 308}