# teeu — Очереди и планировщик

## Драйвер

`QUEUE_CONNECTION=database` (Redis в текущем окружении нет). Бизнес-логика к Redis не привязана —
переключение на Redis/Horizon позже без изменения джобов. См. [architecture.md](architecture.md).

## Логические очереди (ТЗ §48)

Имена — в enum `App\Enums\QueueName`. Даже на одном воркере работа логически разделяется; в проде
можно поднять отдельные воркеры под тяжёлые очереди.

| Очередь | Назначение |
|---|---|
| `default` | прочие задачи |
| `yml-download` | скачивание YML-фидов (SSRF-ограничения, таймауты) |
| `yml-processing` | парсинг/стейджинг/классификация/upsert офферов |
| `ai` | запросы к DeepSeek (модерация, классификация) |
| `images` | скачивание и конвертация изображений в WebP |
| `notifications` | in-app / email / telegram / web-push |

Джоб указывает очередь через `->onQueue(QueueName::Ai->value)` или свойство `$queue` в конструкторе.
Retry-политика (timeout/tries/backoff) задаётся **per job** — единой политики для Telegram / images /
AI / YML быть не должно (ТЗ §48.2).

## Запуск (dev)

```bash
php artisan queue:work --queue=default,ai,images,yml-processing,yml-download,notifications
php artisan schedule:work
```

## Планировщик (ТЗ §49) — реализовано

Определён в `routes/console.php`, все `->withoutOverlapping()`:

| Расписание | Команда | Назначение |
|---|---|---|
| `everyTenMinutes` | `teeu:yml:sync-due` | ставит в очередь due-фиды (ТЗ §50) |
| `everyFiveMinutes` | `teeu:wildberries:prices` | `PullWildberriesPricesJob` (`yml-download`, уникальна по фиду) кабинетам WB с открытым окном цен — по базовому токену WB отдаёт цены раз в 15 минут ([marketplace-import.md](marketplace-import.md)) |
| `hourly` | `teeu:orders:send-stale-reminders` | напоминания по new-заказам >48ч (§29.2) |
| `everyThirtyMinutes` | `teeu:ai:retry-failed` | fail-closed retry контента, зависшего из-за сбоя AI (§12.5) |
| `dailyAt('04:00')` | `teeu:sitemap:generate` | перегенерация статических sitemap в `public/` (§41.5) |
| `everyThirtyMinutes` + `runInBackground` | `teeu:catalog:warm` | прогрев кэша фильтров и счётчиков корневых и крупных разделов каталога — пересчёт на секунды не достаётся посетителю ([catalog.md](catalog.md)); ещё раз запускается в фоне после деплоя |
| `everyFiveMinutes` + `runInBackground` | `teeu:ai:rewrite-descriptions` | **обособленный воркер AI-описаний** (§16): берёт случайную пачку ещё не обработанных товаров (равномерно по продавцам) и генерит уникальное описание в `products.ai_description`. **По умолчанию выключен** — гейт через настройку `ai.rewrite.enabled` (админка → «AI-описания»). Отдельный процесс, не блокирует `schedule:run`. |

Прочие команды обслуживания (ручные): `teeu:yml:sync {feed}`, `teeu:products:rebuild-rating`,
`teeu:attributes:reindex` (бэкофилл фильтров каталога — [catalog.md](catalog.md)),
`teeu:ai:rewrite-descriptions --limit=N` (разовый прогон AI-описаний). Все идемпотентны, с
корректными exit-кодами (ТЗ §59).

> **ВАЖНО (прод, Hestia):** строка cron с `schedule:run` должна иметь префикс `-d disable_functions=`,
> иначе `withoutOverlapping`-команды падают на `pcntl_signal()` и молча пропускаются. Детали и разбор
> инцидента — [deployment.md](deployment.md).

## Воркеры на проде: очереди разведены по процессам

Супервайзора на сервере нет — воркеры поднимает cron под `flock`, каждый со своим замком. Процессов
**четыре**, и это не про производительность, а про то, чтобы одна очередь не съедала другую.

| Замок | Очереди |
|---|---|
| `teeu-queue.lock` | `yml-download`, `yml-processing`, `default` |
| `teeu-queue-fast.lock` | `notifications`, `images` |
| `teeu-queue-ai.lock`, `teeu-queue-ai2.lock` | `ai` (две линии) |

**Почему разведены.** Список в `--queue` — это приоритет, а не набор: пока в первой очереди есть
задачи, до второй воркер не дойдёт вообще. Когда `ai` стояла первой в одном списке с `yml-*`,
двадцать три тысячи задач модерации остановили импорт фидов на несколько часов — при скорости
разбора около пятидесяти задач в минуту очередь означала восемь часов простоя импорта.

**Почему у `ai` две линии.** Одна разбирает примерно пятьдесят задач в минуту, и накопленный после
подключения нескольких крупных фидов хвост ей не по силам. Больше двух ставить некуда: на сервере два
ядра, и он делит их с соседним проектом — упрёмся не в очередь, а в процессор.

**Хвост модерации конечен.** На модерацию товар уходит только при изменившемся тексте
(`YmlImportService`), поэтому повторные импорты очередь не наполняют — большая очередь означает
подключение новых фидов, а не бесконечную работу.

## Постановка импорта: одна задача на фид

`RunYmlImportJob` реализует `ShouldBeUnique` по номеру фида. Без этого очередь при заторе растёт сама
от себя: `next_sync_at` обновляется по факту импорта, а `teeu:yml:sync-due` ставит просроченные фиды
каждые десять минут — фид, ждущий в очереди, остаётся просроченным и ставится снова. На проде
восемнадцать фидов так превратились в семьдесят задач, каждая заново качала тот же файл.

Замок держится час (`uniqueFor`): обычный импорт укладывается в минуты, но крупный фид с медленным
сервером продавца бывает и дольше, а вечный замок после аварийного завершения хуже дубля.

Полный пример cron — [deployment.md](deployment.md).
