< Summary

Information
Class: Services.SearchServices.OrderSearch.OrderSearchSyncService
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/SearchServices/OrderSearch/OrderSearchSyncService.cs
Line coverage
88%
Covered lines: 156
Uncovered lines: 20
Coverable lines: 176
Total lines: 289
Line coverage: 88.6%
Branch coverage
79%
Covered branches: 27
Total branches: 34
Branch coverage: 79.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Project(...)100%11100%
SyncOrderAsync()0%620%
SyncBatchAsync()100%44100%
FullReindexAsync()75%8892.85%
RepairDriftAsync()85%202091.95%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/SearchServices/OrderSearch/OrderSearchSyncService.cs

#LineLine coverage
 1using System.Diagnostics;
 2using Microsoft.EntityFrameworkCore;
 3using Microsoft.Extensions.Configuration;
 4using Microsoft.Extensions.Logging;
 5using OzonDomains.Models;
 6using OzonRepositories.Context;
 7
 8namespace Services.SearchServices.OrderSearch;
 9
 10public class OrderSearchSyncService : IOrderSearchSyncService
 11{
 12    private const int FullReindexBatchSize = 2000;
 13    private const int DriftRepairBatchSize = 2000;
 14    private const int DriftRepairDeleteBatchSize = 500;
 15
 16    private readonly ApplicationDbContext _context;
 17    private readonly IOrderSearchService _searchService;
 18    private readonly IOrderSearchDriftChecker _driftChecker;
 19    private readonly IConfiguration _configuration;
 20    private readonly ILogger<OrderSearchSyncService> _logger;
 21    private readonly IOrderSearchDiagnosticsStore _diagnostics;
 22
 1123    public OrderSearchSyncService(
 1124        ApplicationDbContext context,
 1125        IOrderSearchService searchService,
 1126        IOrderSearchDriftChecker driftChecker,
 1127        IConfiguration configuration,
 1128        ILogger<OrderSearchSyncService> logger,
 1129        IOrderSearchDiagnosticsStore diagnostics)
 30    {
 1131        _context = context;
 1132        _searchService = searchService;
 1133        _driftChecker = driftChecker;
 1134        _configuration = configuration;
 1135        _logger = logger;
 1136        _diagnostics = diagnostics;
 1137    }
 38
 39    private static IQueryable<OrderSearchDocument> Project(IQueryable<Order> orders) =>
 1140        orders.Select(o => new OrderSearchDocument
 1141        {
 1142            Id = o.Id,
 1143            ShipmentNumber = o.ShipmentNumber,
 1144            Scanit = o.Scanit,
 1145            ProductName = o.ProductName,
 1146            Article = o.Article,
 1147            ProductKey = o.ProductKey,
 1148            AppStatusName = o.AppStatus != null ? o.AppStatus.Name : null,
 1149            SupplierName = o.Supplier != null ? o.Supplier.Name : null,
 1150            ManufacturerName = o.EtProducer != null ? o.EtProducer.Name : null,
 1151            ShipmentWarehouseName = o.ShipmentWarehouse != null ? o.ShipmentWarehouse.Name : null,
 1152            OzonClientName = o.OzonClient != null ? o.OzonClient.Name : null,
 1153            IsArchived = o.OrderArchive != null,
 1154            UpdatedAt = o.UpdatedAt
 1155        });
 56
 57    public async Task SyncOrderAsync(int orderId)
 58    {
 59        try
 60        {
 061            var document = await Project(_context.Orders.AsNoTracking().Where(o => o.Id == orderId))
 062                .FirstOrDefaultAsync();
 63
 064            if (document == null)
 65            {
 066                await _searchService.DeleteFromIndexAsync(orderId);
 067                return;
 68            }
 69
 070            await _searchService.IndexOrderAsync(document);
 071        }
 072        catch (Exception ex)
 73        {
 074            _logger.LogWarning(ex, "Не удалось переиндексировать заказ {OrderId} — подхватит incremental sync", orderId)
 075        }
 076    }
 77
 78    public async Task<OrderSearchSyncCheckpoint?> SyncBatchAsync(
 79        DateTime sinceUtc,
 80        int sinceOrderId,
 81        int batchSize)
 82    {
 683        var batch = await Project(_context.Orders.AsNoTracking()
 684                .Where(o => o.UpdatedAt != null &&
 685                    (o.UpdatedAt > sinceUtc ||
 686                     (o.UpdatedAt == sinceUtc && o.Id > sinceOrderId)))
 687                .OrderBy(o => o.UpdatedAt)
 688                .ThenBy(o => o.Id))
 689            .Take(batchSize)
 690            .ToListAsync();
 91
 692        if (batch.Count == 0)
 93        {
 194            return null;
 95        }
 96
 597        var ok = await _searchService.BulkIndexOrdersAsync(batch);
 598        if (!ok)
 99        {
 1100            _logger.LogWarning("Incremental sync: bulk-индексация {Count} заказов завершилась с ошибками", batch.Count);
 101            // Нельзя подтверждать checkpoint: иначе заказы из неуспешного пакета будут
 102            // навсегда пропущены следующими запусками incremental sync.
 1103            throw new InvalidOperationException(
 1104                $"Incremental sync: не удалось проиндексировать пакет из {batch.Count} заказов.");
 105        }
 106
 4107        var last = batch[^1];
 4108        return new OrderSearchSyncCheckpoint(last.UpdatedAt!.Value, last.Id);
 5109    }
 110
 111    public async Task FullReindexAsync()
 112    {
 2113        var sw = Stopwatch.StartNew();
 2114        if (!await _searchService.DeleteIndexAsync())
 115        {
 0116            throw new InvalidOperationException("Полная переиндексация: не удалось удалить предыдущий индекс.");
 117        }
 118
 2119        if (!await _searchService.CreateIndexAsync())
 120        {
 0121            throw new InvalidOperationException("Полная переиндексация: не удалось создать индекс.");
 122        }
 123
 2124        var processed = 0;
 2125        var lastId = 0;
 126
 2127        while (true)
 128        {
 4129            var batch = await Project(_context.Orders.AsNoTracking()
 4130                    .Where(o => o.Id > lastId)
 4131                    .OrderBy(o => o.Id))
 4132                .Take(FullReindexBatchSize)
 4133                .ToListAsync();
 134
 4135            if (batch.Count == 0)
 136            {
 137                break;
 138            }
 139
 3140            if (!await _searchService.BulkIndexOrdersAsync(batch))
 141            {
 1142                throw new InvalidOperationException(
 1143                    $"Полная переиндексация: не удалось проиндексировать пакет из {batch.Count} заказов.");
 144            }
 145
 2146            processed += batch.Count;
 2147            lastId = batch[^1].Id;
 2148        }
 149
 1150        _diagnostics.RecordIndex(
 1151            OrderSearchIndexOperation.FullReindex,
 1152            processed,
 1153            sw.ElapsedMilliseconds,
 1154            true,
 1155            $"Полная переиндексация: {processed} заказов");
 156
 1157        _logger.LogInformation("OrderSearch full reindex: проиндексировано {Count} заказов", processed);
 1158    }
 159
 160    public async Task<OrderSearchDriftRepairResult> RepairDriftAsync(
 161        string trigger,
 162        CancellationToken cancellationToken = default)
 163    {
 4164        if (!_configuration.GetValue<bool>("OrdersSearch:UseElasticsearch"))
 165        {
 1166            return new OrderSearchDriftRepairResult
 1167            {
 1168                Skipped = true,
 1169                Trigger = trigger,
 1170                Message = "Elasticsearch выключен (OrdersSearch:UseElasticsearch=false)."
 1171            };
 172        }
 173
 3174        if (!await _searchService.IndexExistsAsync())
 175        {
 0176            await _searchService.CreateIndexAsync();
 177        }
 178
 3179        var before = await _driftChecker.CheckAsync($"{trigger}/Before", cancellationToken);
 3180        if (before.Skipped)
 181        {
 0182            return new OrderSearchDriftRepairResult
 0183            {
 0184                Skipped = true,
 0185                Trigger = trigger,
 0186                Message = before.Message
 0187            };
 188        }
 189
 3190        if (before.AbsoluteDrift == 0)
 191        {
 1192            return new OrderSearchDriftRepairResult
 1193            {
 1194                Skipped = true,
 1195                Trigger = trigger,
 1196                Message = "Расхождение отсутствует — исправление не требуется.",
 1197                IndexCountBefore = before.IndexCount,
 1198                DbOrderCountBefore = before.DbOrderCount,
 1199                AbsoluteDriftBefore = before.AbsoluteDrift,
 1200                IndexCountAfter = before.IndexCount,
 1201                DbOrderCountAfter = before.DbOrderCount,
 1202                AbsoluteDriftAfter = before.AbsoluteDrift,
 1203                IsAlertAfter = before.IsAlert
 1204            };
 205        }
 206
 2207        var sw = Stopwatch.StartNew();
 2208        var indexedMissing = 0;
 2209        var deletedOrphans = 0;
 210
 2211        var dbIds = await _context.Orders.AsNoTracking()
 2212            .OrderBy(o => o.Id)
 2213            .Select(o => o.Id)
 2214            .ToListAsync(cancellationToken);
 2215        var dbIdSet = dbIds.ToHashSet();
 216
 2217        var processed = 0;
 4218        while (processed < dbIds.Count)
 219        {
 2220            var idBatch = dbIds.Skip(processed).Take(DriftRepairBatchSize).ToList();
 2221            processed += idBatch.Count;
 222
 2223            var existing = await _searchService.GetExistingOrderIdsAsync(idBatch, cancellationToken);
 10224            var missingIds = idBatch.Where(id => !existing.Contains(id)).ToList();
 2225            if (missingIds.Count == 0)
 226            {
 227                continue;
 228            }
 229
 1230            var documents = await Project(_context.Orders.AsNoTracking().Where(o => missingIds.Contains(o.Id)))
 1231                .ToListAsync(cancellationToken);
 232
 1233            if (documents.Count == 0)
 234            {
 235                continue;
 236            }
 237
 1238            if (await _searchService.BulkIndexOrdersAsync(documents))
 239            {
 1240                indexedMissing += documents.Count;
 241            }
 1242        }
 243
 2244        var indexedIds = await _searchService.GetAllIndexedOrderIdsAsync(cancellationToken);
 12245        var orphanIds = indexedIds.Where(id => !dbIdSet.Contains(id)).ToList();
 246
 6247        for (var i = 0; i < orphanIds.Count; i += DriftRepairDeleteBatchSize)
 248        {
 1249            var deleteBatch = orphanIds.Skip(i).Take(DriftRepairDeleteBatchSize).ToList();
 1250            deletedOrphans += await _searchService.BulkDeleteFromIndexAsync(deleteBatch, cancellationToken);
 251        }
 252
 2253        var after = await _driftChecker.CheckAsync(trigger, cancellationToken);
 254
 2255        var message = after.AbsoluteDrift > 0
 2256            ? $"Исправление завершено с остаточным расхождением: доиндексировано {indexedMissing:N0}, удалено сирот {del
 2257            : $"Расхождение исправлено: доиндексировано {indexedMissing:N0}, удалено сирот {deletedOrphans:N0}. {after.M
 258
 2259        _diagnostics.RecordIndex(
 2260            OrderSearchIndexOperation.DriftRepair,
 2261            indexedMissing + deletedOrphans,
 2262            sw.ElapsedMilliseconds,
 2263            after.AbsoluteDrift == 0,
 2264            message);
 265
 2266        _logger.LogInformation(
 2267            "OrderSearch drift repair ({Trigger}): indexed={Indexed}, deleted={Deleted}, driftBefore={DriftBefore}, drif
 2268            trigger,
 2269            indexedMissing,
 2270            deletedOrphans,
 2271            before.AbsoluteDrift,
 2272            after.AbsoluteDrift);
 273
 2274        return new OrderSearchDriftRepairResult
 2275        {
 2276            Trigger = trigger,
 2277            Message = message,
 2278            IndexCountBefore = before.IndexCount,
 2279            DbOrderCountBefore = before.DbOrderCount,
 2280            AbsoluteDriftBefore = before.AbsoluteDrift,
 2281            IndexedMissing = indexedMissing,
 2282            DeletedOrphans = deletedOrphans,
 2283            IndexCountAfter = after.IndexCount,
 2284            DbOrderCountAfter = after.DbOrderCount,
 2285            AbsoluteDriftAfter = after.AbsoluteDrift,
 2286            IsAlertAfter = after.IsAlert
 2287        };
 4288    }
 289}