75 lines
3.2 KiB
PHP
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);
|
||
|
|
});
|