Files
portal/app/tests/Feature/Advertising/CreativeJobReaperTest.php
T

75 lines
3.2 KiB
PHP
Raw Normal View History

<?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);
});