43e29823a8
Раньше поиск, резолв и изучение делили одну очередь autopodbor — при одном воркере быстрый шаг 2 (изучение) стоял за тяжёлым шагом 1 (поиск с ИИ) другого клиента. Теперь два процесса: • шаг 1 «собрать поле» (поиск + резолв по имени, ИИ+внешние) → autopodbor-find • шаг 2 «дайте источники / изучение» (без ИИ) → autopodbor-study Идут параллельно, друг друга не держат. Замок WithoutOverlapping по runId цел. Запуск теперь двумя воркерами: queue:work redis --queue=autopodbor-find --timeout=3600 queue:work redis --queue=autopodbor-study --timeout=3600 TDD: AutopodborQueueThrottleTest 4/4, автоподбор 434/434. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
105 lines
3.6 KiB
PHP
105 lines
3.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Jobs\Autopodbor;
|
|
|
|
use App\Models\AutopodborCompetitor;
|
|
use App\Models\AutopodborRun;
|
|
use App\Services\Autopodbor\Agent\CompetitorAgent;
|
|
use App\Services\Autopodbor\Agent\Dto\ResolveByNameRequest;
|
|
use App\Services\Autopodbor\AutopodborDedup;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class RunAutopodborResolveJob implements ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
public int $tries = 3;
|
|
|
|
public array $backoff = [15, 60, 300];
|
|
|
|
public function __construct(public int $runId)
|
|
{
|
|
// Резолв по имени — тоже шаг 1 (ИИ-поиск кандидатов), поэтому на одной очереди с поиском.
|
|
$this->onQueue('autopodbor-find');
|
|
}
|
|
|
|
/**
|
|
* Защита от нахлёста: один прогон резолва не обрабатывается двумя воркерами разом (ретрай).
|
|
*
|
|
* @return array<int, object>
|
|
*/
|
|
public function middleware(): array
|
|
{
|
|
return [new WithoutOverlapping((string) $this->runId)];
|
|
}
|
|
|
|
public function handle(CompetitorAgent $agent, AutopodborDedup $dedup): void
|
|
{
|
|
$run = AutopodborRun::findOrFail($this->runId);
|
|
|
|
// Выставляем tenant-контекст сессионно
|
|
DB::statement("SELECT set_config('app.current_tenant_id', ?, false)", [(string) $run->tenant_id]);
|
|
|
|
// Идемпотентность: завершённый прогон повторно не обрабатываем (защита от лишнего ретрая/повторного dispatch).
|
|
if (in_array($run->status, ['done', 'empty', 'failed'], true)) {
|
|
return;
|
|
}
|
|
|
|
$run->update(['status' => 'running', 'started_at' => now()]);
|
|
|
|
try {
|
|
$p = $run->params;
|
|
|
|
$res = $agent->resolveByName(new ResolveByNameRequest(
|
|
name: $p['name'],
|
|
regionCode: (int) $run->region_code,
|
|
));
|
|
|
|
$unique = $dedup->dedupCompetitors($res->candidates);
|
|
|
|
if ($unique === []) {
|
|
$run->update(['status' => 'empty', 'finished_at' => now()]);
|
|
|
|
return;
|
|
}
|
|
|
|
foreach ($unique as $c) {
|
|
AutopodborCompetitor::updateOrCreate(
|
|
[
|
|
'tenant_id' => $run->tenant_id,
|
|
'search_run_id' => $run->id,
|
|
'dedup_key' => $c['dedup_key'],
|
|
],
|
|
[
|
|
'name' => $c['name'],
|
|
'description' => $c['description'] ?? null,
|
|
'is_federal' => (bool) ($c['is_federal'] ?? false),
|
|
'relevance_pct' => null,
|
|
'origin' => 'resolve',
|
|
'site_url' => $c['site_url'] ?? null,
|
|
'directory_urls' => $c['directory_urls'] ?? [],
|
|
'provenance' => $c['provenance'] ?? [],
|
|
]
|
|
);
|
|
}
|
|
|
|
$run->update(['status' => 'done', 'finished_at' => now()]);
|
|
} catch (\Throwable $e) {
|
|
$run->update([
|
|
'status' => 'failed',
|
|
'error_code' => substr($e->getMessage(), 0, 64),
|
|
'finished_at' => now(),
|
|
]);
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|