perf(autopodbor): разнести шаги по очередям — find (ИИ) и study (тупой слой)
Раньше поиск, резолв и изучение делили одну очередь 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>
This commit is contained in:
@@ -27,8 +27,8 @@ class RunAutopodborResolveJob implements ShouldQueue
|
||||
|
||||
public function __construct(public int $runId)
|
||||
{
|
||||
// Та же выделенная очередь, что и поиск — единый глобальный потолок внешних сервисов.
|
||||
$this->onQueue('autopodbor');
|
||||
// Резолв по имени — тоже шаг 1 (ИИ-поиск кандидатов), поэтому на одной очереди с поиском.
|
||||
$this->onQueue('autopodbor-find');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,9 +40,9 @@ class RunAutopodborSearchJob implements ShouldQueue
|
||||
|
||||
public function __construct(public int $runId)
|
||||
{
|
||||
// Выделенная очередь: число её воркеров = глобальный потолок одновременных подборов
|
||||
// (лимиты EXA/xfetch/AITUNNEL — на КЛЮЧ, общий на всех клиентов).
|
||||
$this->onQueue('autopodbor');
|
||||
// Шаг 1 «собрать поле» (ИИ + внешние сервисы) — своя очередь, отдельный воркер-процесс,
|
||||
// чтобы тяжёлый поиск не стоял в одной линии с быстрым шагом 2 (изучением) другого клиента.
|
||||
$this->onQueue('autopodbor-find');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,8 +32,9 @@ class RunAutopodborStudyJob implements ShouldQueue
|
||||
|
||||
public function __construct(public int $runId)
|
||||
{
|
||||
// Та же выделенная очередь, что и поиск — единый глобальный потолок внешних сервисов.
|
||||
$this->onQueue('autopodbor');
|
||||
// Шаг 2 «изучение / дайте источники» — тупой слой (без ИИ). ОТДЕЛЬНАЯ очередь + свой воркер,
|
||||
// чтобы изучение шло параллельно поиску, а не ждало его в общей линии.
|
||||
$this->onQueue('autopodbor-study');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,33 +8,39 @@ use App\Jobs\Autopodbor\RunAutopodborStudyJob;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
// Масштаб: лимиты внешних сервисов — на КЛЮЧ (общий на всех клиентов). Джобы автоподбора идут на
|
||||
// ВЫДЕЛЕННУЮ очередь autopodbor — число её воркеров = глобальный потолок одновременных подборов.
|
||||
// Масштаб: два шага разнесены по РАЗНЫМ очередям (разные процессы-воркеры), чтобы не толкались.
|
||||
// Шаг 1 «собрать поле» с ИИ (поиск + резолв по имени) → очередь autopodbor-find.
|
||||
// Шаг 2 «изучение / дайте источники» — тупой слой → очередь autopodbor-study.
|
||||
// Так быстрый шаг 2 одного клиента не стоит за тяжёлым шагом 1 другого.
|
||||
// Плюс защита от нахлёста (WithoutOverlapping) на случай ретрая/повторного dispatch.
|
||||
|
||||
it('поиск уходит на выделенную очередь autopodbor', function () {
|
||||
it('шаг 1 (поиск) уходит на очередь autopodbor-find', function () {
|
||||
Queue::fake();
|
||||
|
||||
RunAutopodborSearchJob::dispatch(123);
|
||||
|
||||
Queue::assertPushedOn('autopodbor', RunAutopodborSearchJob::class);
|
||||
Queue::assertPushedOn('autopodbor-find', RunAutopodborSearchJob::class);
|
||||
});
|
||||
|
||||
it('поиск защищён от нахлёста — middleware WithoutOverlapping по runId', function () {
|
||||
$mw = (new RunAutopodborSearchJob(123))->middleware();
|
||||
|
||||
expect($mw)->toHaveCount(1)
|
||||
->and($mw[0])->toBeInstanceOf(WithoutOverlapping::class);
|
||||
});
|
||||
|
||||
it('шаг 2 (изучение) и резолв — на той же очереди и защищены от нахлёста', function () {
|
||||
it('резолв по имени — тоже шаг 1, на очереди autopodbor-find', function () {
|
||||
Queue::fake();
|
||||
RunAutopodborStudyJob::dispatch(7);
|
||||
|
||||
RunAutopodborResolveJob::dispatch(8);
|
||||
|
||||
Queue::assertPushedOn('autopodbor', RunAutopodborStudyJob::class);
|
||||
Queue::assertPushedOn('autopodbor', RunAutopodborResolveJob::class);
|
||||
Queue::assertPushedOn('autopodbor-find', RunAutopodborResolveJob::class);
|
||||
});
|
||||
|
||||
expect((new RunAutopodborStudyJob(7))->middleware()[0])->toBeInstanceOf(WithoutOverlapping::class)
|
||||
it('шаг 2 (изучение) уходит на ОТДЕЛЬНУЮ очередь autopodbor-study', function () {
|
||||
Queue::fake();
|
||||
|
||||
RunAutopodborStudyJob::dispatch(7);
|
||||
|
||||
Queue::assertPushedOn('autopodbor-study', RunAutopodborStudyJob::class);
|
||||
});
|
||||
|
||||
it('все три задания защищены от нахлёста — middleware WithoutOverlapping по runId', function () {
|
||||
expect((new RunAutopodborSearchJob(123))->middleware())->toHaveCount(1)
|
||||
->and((new RunAutopodborSearchJob(123))->middleware()[0])->toBeInstanceOf(WithoutOverlapping::class)
|
||||
->and((new RunAutopodborStudyJob(7))->middleware()[0])->toBeInstanceOf(WithoutOverlapping::class)
|
||||
->and((new RunAutopodborResolveJob(8))->middleware()[0])->toBeInstanceOf(WithoutOverlapping::class);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user