From 4f11ed5205faa4118ebf0b3aa52dc6631e21fc81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 22 May 2026 09:26:24 +0300 Subject: [PATCH] =?UTF-8?q?fix(supplier):=20RouteSupplierLeadJob=20=D1=82?= =?UTF-8?q?=D0=B5=D1=80=D0=BC=D0=B8=D0=BD=D0=B0=D0=BB=D0=B5=D0=BD=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20=D0=BE=D1=82=D1=81=D1=83=D1=82=D1=81=D1=82=D0=B2?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=BB=D0=B8=D0=B4=D0=B0=20(=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=BF=20retry-=D1=88=D1=82=D0=BE=D1=80=D0=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit findOrFail -> find + ранний выход при null: 'лид удалён/не существует' — терминальная, не транзиентная ошибка. Раньше ModelNotFoundException -> queue->failed() писал в failed_webhook_jobs -> RetryFailedSupplierJobsCommand бесконечно перезапускал (инцидент 21-22.05: 25k+ записей по удалённому лиду №1). +тест RED->GREEN. Co-Authored-By: Claude Opus 4.7 --- app/app/Jobs/RouteSupplierLeadJob.php | 15 ++++++++++++++- .../Feature/Jobs/RouteSupplierLeadJobTest.php | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/app/Jobs/RouteSupplierLeadJob.php b/app/app/Jobs/RouteSupplierLeadJob.php index 917222b7..4268cb2f 100644 --- a/app/app/Jobs/RouteSupplierLeadJob.php +++ b/app/app/Jobs/RouteSupplierLeadJob.php @@ -91,7 +91,20 @@ class RouteSupplierLeadJob implements ShouldQueue LeadDistributor $distributor, RegionTagResolver $tagResolver, ): void { - $lead = SupplierLead::findOrFail($this->supplierLeadId); + $lead = SupplierLead::find($this->supplierLeadId); + + // Терминальный случай: лид удалён/не существует — это НЕ транзиентная ошибка, + // повтор бессмыслен. НЕ бросаем ModelNotFoundException: иначе queue->failed() + // пишет строку в failed_webhook_jobs, а RetryFailedSupplierJobsCommand + // бесконечно перезапускает job (retry-шторм, инцидент 21-22.05.2026 — + // 25k+ записей по удалённому лиду №1). + if ($lead === null) { + Log::warning('supplier_lead.not_found_terminal', [ + 'supplier_lead_id' => $this->supplierLeadId, + ]); + + return; + } // Idempotency guard для retry-сценария ($tries = 3). // Если лид уже обработан — выходим, не создаём ghost duplicate'ы deal'ов. diff --git a/app/tests/Feature/Jobs/RouteSupplierLeadJobTest.php b/app/tests/Feature/Jobs/RouteSupplierLeadJobTest.php index 5bfc4f4b..b12af59f 100644 --- a/app/tests/Feature/Jobs/RouteSupplierLeadJobTest.php +++ b/app/tests/Feature/Jobs/RouteSupplierLeadJobTest.php @@ -48,6 +48,21 @@ function runRouteJob(int $supplierLeadId): void // `linkProjectToSupplier` helper now lives in tests/Pest.php — single source. +it('is terminal (does not throw / re-queue) when the supplier lead does not exist', function (): void { + // Регрессия retry-шторма 21-22.05.2026: RouteSupplierLeadJob для удалённого лида №1 + // бросал ModelNotFoundException -> queue->failed() писал в failed_webhook_jobs -> + // RetryFailedSupplierJobsCommand бесконечно перезапускал (25k+ записей). + // «Лид не найден» — терминальная (не транзиентная) ошибка: повтор бессмыслен. + $missingId = 999999; + expect(SupplierLead::find($missingId))->toBeNull(); + + // Не должно бросать исключение (иначе сработает failed() -> retry-цикл). + runRouteJob($missingId); + + // Никаких побочных эффектов. + expect(Deal::count())->toBe(0); +}); + it('routes 1 lead to N tenants — creates N deal copies (sharing-model)', function (): void { $supplier = SupplierProject::factory()->create([ 'platform' => 'B1',