Files
portal/app/tests/Feature/Advertising/CreativeJobReaperTest.php
T
Дмитрий 542b7d2d22 fix реклама за показы: очередь робота больше не встаёт колом, доклад об успехе не врёт, тихий ноль ловится
Р6 — задание, брошенное «в работе», держало очередь для всех клиентов навсегда.
Причина сбоя обрезается до 900 знаков, иначе портал отвергал отчёт о самом частом
виде поломки. Провал отчёта больше не глотается молча — едет в письмо человеку.
Приём отчёта ловит любую ошибку и закрывает задание сбойным вместо 500 роботу.
Новый сторож creative-jobs:reap каждые 10 минут разбирает пробку.

Р7 — обрыв на докладе «готово» объявлял провалом уже сделанную работу. Доклад
вынесен из-под обработчика сбоя, повторяется трижды, при неудаче только письмо.
Осечка ухода со страницы после «Создать» больше не считается провалом загрузки.

Р8 — пустой список размеров считался успехом, и робот заливал одни и те же файлы
по кругу, оставляя мусор в живом кабинете. Теперь громкий отказ до похода в Яндекс.

Портал 274/274, робот 41/41. Все защиты проверены вырезанием.
Выходов снятия заморозки денег по-прежнему четыре.
2026-07-27 20:42:23 +03:00

75 lines
3.2 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\AdCampaign;
use App\Models\AdCreativeJob;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\DatabaseTransactions;
uses(DatabaseTransactions::class);
/**
* Сторож зависших заданий робота-грузчика.
*
* Задание «в работе» — это пробка на всю очередь: пока хоть одно в работе, выдача отвечает
* «работы нет» ВСЕМ клиентам, и ни одна кампания больше не стартует. Робот может умереть
* молча (упал процесс, перезагрузили сервер, портал отверг отчёт) — и тогда пробка вечная.
* Сторож её разбирает: вернуть в очередь, а после исчерпания попыток — честно закрыть сбоем,
* чтобы не крутить один и тот же безнадёжный круг.
*/
function makeStuckJob(array $attributes = []): AdCreativeJob
{
$tenant = Tenant::factory()->create();
$campaign = AdCampaign::create([
'tenant_id' => $tenant->id, 'name' => 'C', 'mode' => AdCampaign::MODE_MANUAL,
'audience_days' => 10, 'client_cpm_rub' => '120.00',
]);
return AdCreativeJob::create(array_merge([
'tenant_id' => $tenant->id,
'campaign_id' => $campaign->id,
'status' => AdCreativeJob::STATUS_TAKEN,
'attempts' => 1,
'snapshot_before' => [],
'taken_at' => now()->subMinutes(40),
], $attributes));
}
it('returns a job abandoned in flight back to the queue', function () {
$job = makeStuckJob();
$this->artisan('creative-jobs:reap')->assertExitCode(0);
expect($job->fresh()->status)->toBe(AdCreativeJob::STATUS_QUEUED)
->and($job->fresh()->taken_at)->toBeNull();
});
it('leaves a job that was taken just now alone', function () {
// Робот работает минутами: качает файлы, поднимает браузер, грузит 15 креативов.
// Отобрать у него задание на середине — значит получить два робота на одной кампании.
$job = makeStuckJob(['taken_at' => now()->subMinutes(3)]);
$this->artisan('creative-jobs:reap')->assertExitCode(0);
expect($job->fresh()->status)->toBe(AdCreativeJob::STATUS_TAKEN);
});
it('closes the job as failed once the attempts are spent instead of looping forever', function () {
$job = makeStuckJob(['attempts' => 3]);
$this->artisan('creative-jobs:reap')->assertExitCode(0);
expect($job->fresh()->status)->toBe(AdCreativeJob::STATUS_FAILED)
->and($job->fresh()->failure_reason)->toContain('не отчитался')
->and($job->fresh()->finished_at)->not->toBeNull();
});
it('does not touch jobs that are not in flight', function () {
$job = makeStuckJob(['status' => AdCreativeJob::STATUS_DONE, 'taken_at' => now()->subDays(3)]);
$this->artisan('creative-jobs:reap')->assertExitCode(0);
expect($job->fresh()->status)->toBe(AdCreativeJob::STATUS_DONE);
});