From bd15854b42b1ace65b311e97baa6bffcc04423f7 Mon Sep 17 00:00:00 2001 From: nurdotnet <278048682+nurdotnet@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:30:37 +0500 Subject: [PATCH] =?UTF-8?q?fix(moysklad/import):=20per-page=20retry=20+=20?= =?UTF-8?q?=D1=87=D0=B0=D1=89=D0=B5=20SaveChanges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Почему импорт раньше обрывался на ~9500/29500 товаров: - StreamPagedAsync бросал исключение при любом сетевом глюке или таймауте HttpClient (90s) на одной из страниц и весь цикл сыпался. - Флаш делался раз в 500 товаров, так что при обрыве на 9500-м можно было потерять последние 499. Фиксы: - Per-page retry до 5 раз с exp-backoff (2,4,8,16с) — обрабатываем только сетевые ошибки (HttpRequestException / TaskCanceledException / IOException). API-ошибки типа 4xx проходят наверх как есть. - SaveChangesAsync теперь каждые 100 товаров вместо 500 — меньше вероятность потерять при внезапном обрыве на границе. - При исчерпании retries — бросаем осмысленное исключение с offset'ом. Пользователь сейчас имеет 9500 из 29509 товаров (группа "Алкоголь" — 20 из 518). Нужно перезапустить импорт в UI с overwriteExisting=true — существующие товары обновит, недостающие подтянет. --- .../Integrations/MoySklad/MoySkladClient.cs | 33 ++++++++++++++++--- .../MoySklad/MoySkladImportService.cs | 5 +-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/food-market.infrastructure/Integrations/MoySklad/MoySkladClient.cs b/src/food-market.infrastructure/Integrations/MoySklad/MoySkladClient.cs index bed9460..d88f5f9 100644 --- a/src/food-market.infrastructure/Integrations/MoySklad/MoySkladClient.cs +++ b/src/food-market.infrastructure/Integrations/MoySklad/MoySkladClient.cs @@ -98,14 +98,39 @@ public async Task> GetAllFoldersAsync(string token, Cancel [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct) { const int pageSize = 1000; + const int maxAttempts = 5; var offset = 0; var filterSuffix = archivedOnly ? "&filter=archived=true" : ""; while (true) { - using var req = Build(HttpMethod.Get, $"{path}?limit={pageSize}&offset={offset}{filterSuffix}", token); - using var res = await _http.SendAsync(req, ct); - res.EnsureSuccessStatusCode(); - var page = await res.Content.ReadFromJsonAsync>(Json, ct); + MsListResponse? page = null; + Exception? lastErr = null; + for (var attempt = 1; attempt <= maxAttempts; attempt++) + { + try + { + using var req = Build(HttpMethod.Get, $"{path}?limit={pageSize}&offset={offset}{filterSuffix}", token); + using var res = await _http.SendAsync(req, ct); + res.EnsureSuccessStatusCode(); + page = await res.Content.ReadFromJsonAsync>(Json, ct); + lastErr = null; + break; + } + catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or IOException) + { + lastErr = ex; + if (attempt == maxAttempts) break; + // Exponential-ish backoff: 2s, 4s, 8s, 16s. + await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), ct); + } + } + if (lastErr is not null) + { + // Re-throw after retries so the caller sees a real failure instead of silent halt. + throw new InvalidOperationException( + $"MoySklad paging failed at {path} offset={offset} after {maxAttempts} attempts: {lastErr.Message}", + lastErr); + } if (page is null || page.Rows.Count == 0) yield break; foreach (var row in page.Rows) yield return row; if (page.Rows.Count < pageSize) yield break; diff --git a/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs b/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs index 89ec7bb..0f9b8ed 100644 --- a/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs +++ b/src/food-market.infrastructure/Integrations/MoySklad/MoySkladImportService.cs @@ -273,8 +273,9 @@ await foreach (var p in _client.StreamProductsAsync(token, ct)) created++; } - // Flush periodically to keep change tracker light. - if ((created + updated) % 500 == 0) await _db.SaveChangesAsync(ct); + // Flush чаще (каждые 100) чтобы при сетевом обрыве на следующей странице + // мы сохранили как можно больше и смогли безопасно продолжить с overwrite. + if ((created + updated) % 100 == 0) await _db.SaveChangesAsync(ct); } catch (Exception ex) {