| | | 1 | | using Hangfire; |
| | | 2 | | using OzonDomains.Models; |
| | | 3 | | using OzonRepositories.Context; |
| | | 4 | | using Services.SignalRServices; |
| | | 5 | | using Services.HangfireService; |
| | | 6 | | |
| | | 7 | | namespace Services.SearchServices.OrderSearch; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// Ночной safety net: полная переиндексация заказов в Elasticsearch. Ловит удаления заказов |
| | | 11 | | /// (Order.UpdatedAt-триггер их не фиксирует) и любые расхождения, пропущенные enqueue- и |
| | | 12 | | /// incremental-путями синхронизации. |
| | | 13 | | /// </summary> |
| | | 14 | | public class OrderSearchFullReindexJob |
| | | 15 | | { |
| | | 16 | | public const string JobId = "OrderSearchFullReindexJob"; |
| | | 17 | | public const string DefaultCron = "0 3 * * *"; |
| | | 18 | | public const string QueueName = HangfireQueueNames.OrderSearch; |
| | | 19 | | |
| | | 20 | | private readonly ApplicationDbContext _context; |
| | | 21 | | private readonly IOrderSearchSyncService _syncService; |
| | | 22 | | private readonly IOrderSearchDriftChecker _driftChecker; |
| | | 23 | | private readonly IOrderSearchDiagnosticsStore _diagnostics; |
| | | 24 | | private readonly IOrderSearchIndexOperationLock _indexOperationLock; |
| | | 25 | | |
| | 1 | 26 | | public OrderSearchFullReindexJob( |
| | 1 | 27 | | ApplicationDbContext context, |
| | 1 | 28 | | IOrderSearchSyncService syncService, |
| | 1 | 29 | | IOrderSearchDriftChecker driftChecker, |
| | 1 | 30 | | IOrderSearchDiagnosticsStore diagnostics, |
| | 1 | 31 | | IOrderSearchIndexOperationLock indexOperationLock) |
| | | 32 | | { |
| | 1 | 33 | | _context = context; |
| | 1 | 34 | | _syncService = syncService; |
| | 1 | 35 | | _driftChecker = driftChecker; |
| | 1 | 36 | | _diagnostics = diagnostics; |
| | 1 | 37 | | _indexOperationLock = indexOperationLock; |
| | 1 | 38 | | } |
| | | 39 | | |
| | | 40 | | [Queue(QueueName)] |
| | | 41 | | public async Task<string> ExecuteAsync() |
| | | 42 | | { |
| | 1 | 43 | | var operationLock = _indexOperationLock.TryAcquire(); |
| | 1 | 44 | | if (operationLock == null) |
| | | 45 | | { |
| | 1 | 46 | | return "Полная переиндексация ES пропущена: другая операция с индексом уже выполняется."; |
| | | 47 | | } |
| | | 48 | | |
| | 0 | 49 | | using (operationLock) |
| | | 50 | | { |
| | 0 | 51 | | var startedAt = DateTime.UtcNow; |
| | 0 | 52 | | await _syncService.FullReindexAsync(); |
| | | 53 | | |
| | 0 | 54 | | var state = await _context.OrderSearchSyncStates.FindAsync(1); |
| | 0 | 55 | | var isNew = state == null; |
| | 0 | 56 | | state ??= new OrderSearchSyncState { Id = 1 }; |
| | | 57 | | |
| | 0 | 58 | | var now = DateTime.UtcNow; |
| | 0 | 59 | | state.LastFullReindexAt = now; |
| | 0 | 60 | | state.LastSyncedAt = now; |
| | 0 | 61 | | state.LastSyncedOrderId = 0; |
| | | 62 | | |
| | 0 | 63 | | if (isNew) |
| | | 64 | | { |
| | 0 | 65 | | _context.OrderSearchSyncStates.Add(state); |
| | | 66 | | } |
| | | 67 | | |
| | 0 | 68 | | await _context.SaveChangesAsync(); |
| | | 69 | | |
| | 0 | 70 | | var drift = await _driftChecker.CheckAsync("FullReindex"); |
| | | 71 | | |
| | 0 | 72 | | var ev = _diagnostics.GetIndexEvents(10) |
| | 0 | 73 | | .FirstOrDefault(e => e.Operation == OrderSearchIndexOperation.FullReindex && e.TimestampUtc >= startedAt |
| | 0 | 74 | | if (ev != null) |
| | | 75 | | { |
| | 0 | 76 | | var chatMsg = $"Полная переиндексация ES: {ev.Count:N0} заказов за {ev.DurationMs / 1000.0:F1} с"; |
| | 0 | 77 | | if (drift.IsAlert) |
| | 0 | 78 | | chatMsg += $" ⚠ расхождение: {drift.Message}"; |
| | 0 | 79 | | try { await NotificationService.NotifyAsync("ES Индекс", "#8B5CF6", chatMsg, "fa-rotate"); } |
| | 0 | 80 | | catch { /* не роняем джоб */ } |
| | | 81 | | } |
| | | 82 | | |
| | 0 | 83 | | return drift.IsAlert |
| | 0 | 84 | | ? $"OrderSearch full reindex completed at {now:O}. ALERT: {drift.Message}" |
| | 0 | 85 | | : $"OrderSearch full reindex completed at {now:O}. {drift.Message}"; |
| | | 86 | | } |
| | 1 | 87 | | } |
| | | 88 | | } |