skeeks/cms-job отвечает за постановку и выполнение фоновых заданий, историю,
прогресс, отмену, повторные попытки и блокировки ресурсов. Расписания принадлежат
skeeks/cms-agent, а запуск и перезапуск служб — хостингу или администратору.
- Канал — именованная очередь, например
catalogилиmaintenance. - Тип задания — зарегистрированная операция с обработчиком, каналом и правилами выполнения.
- Запуск — конкретная операция с payload и записью
CmsJobRunв истории. - Диспетчер — один ожидающий PHP-процесс, запускающий отдельного ребёнка для каждого задания.
Доменному коду не нужно резервировать сообщения, создавать собственный worker
или вызывать классы yii2-queue: используйте Yii::$app->jobs.
Пакет регистрирует три типа в очереди maintenance: cms-job.cleanup
(просроченная история, раз в сутки) и cms-job.cleanup-logs (просроченные
приватные логи и CSV отчёты об ошибках, раз в час), cms-job.cleanup-workspaces
(временные рабочие папки завершённых заданий, раз в час). При установленном
skeeks/cms-agent >= 3.2.5 их расписания входят в общий конфиг пакета.
После обновления выполните php yii cmsAgent/init: повторный запуск не
создаёт дубли и сохраняет состояние ранее отключённых расписаний.
Нужны работающие cmsAgent/execute и потребитель очереди maintenance.
Очистки используют общий ресурсный lock и отдельные ключи дедупликации на
всю установку, обрабатывают данные порциями до 500 объектов каждого вида
и продолжают тот же запуск, пока просроченные данные не закончатся.
Незавершённые задания защищены; действуют существующие сроки хранения.
Для новых job со штатными приватными логами дополнительная очистка не нужна.
Временные исходники и промежуточные результаты новых job размещайте через
$context->getWorkspace()->path('offers.jsonl'). Пакет сохраняет папку между
продолжениями, защищает её блокировкой и очищает через 7 суток после успеха
или через 14 суток после предупреждений/ошибки/отмены/тайм-аута.
API, ограничения, dry-run и переход со старых папок описаны в WORKSPACES.md.
Незарегистрированные старые папки, включая supplier-imports, и файлы CMS storage
автоматически не удаляются.
Подробности внедрения и ручные команды — в DEPLOYMENT.md.
В общем конфиге проекта или пакета-потребителя, который загружают и web, и console, добавьте:
return [
'components' => [
'jobQueueFactory' => [
'queues' => ['examples' => []],
],
'jobRegistry' => [
'types' => [
'example.calculate-total' => [
'type' => 'example.calculate-total',
'title' => 'Подсчёт суммы',
'handler' => \app\jobs\CalculateTotalJobHandler::class,
'queue' => 'examples',
'timeout' => 60,
'leaseSeconds' => 30,
'idempotent' => true,
'maxAttempts' => 3,
],
],
],
],
];Для Composer-пакета подключите этот файл через extra.config-plugin.web и
extra.config-plugin.console, как это сделано в composer.json.
Не регистрируйте канал только в web-конфиге: консольный диспетчер его не увидит.
Ядро объявляет default и maintenance; остальные каналы объявляет потребитель.
Регистрация канала сама по себе не запускает процесс и не создаёт подключение БД.
Используйте стабильные имена типов и каналов. Для управления службами хостинга
имя канала должно начинаться с буквы/цифры, содержать только буквы, цифры,
-, _ и иметь длину до 64 символов. Обработчики должны быть доступны через
Composer autoload. Несовместимый payload оформляйте новым типом, например .v2.
Ниже полностью исполняемый учебный пример без внешних побочных действий:
namespace app\jobs;
use skeeks\cms\job\contracts\JobReporterInterface;
use skeeks\cms\job\handlers\AbstractJobHandler;
use skeeks\cms\job\runtime\JobContext;
final class CalculateTotalJobHandler extends AbstractJobHandler
{
public function run(JobContext $context, JobReporterInterface $reporter): void
{
$values = $context->get('values', []);
if (!is_array($values)) {
throw new \InvalidArgumentException('Ожидался список чисел.');
}
$reporter->setStage('calculate', 'Подсчёт суммы');
$reporter->setTotal(count($values));
$total = 0;
foreach ($values as $value) {
$reporter->heartbeat();
if ($reporter->isCancelled()) { return; }
if (!is_numeric($value)) {
throw new \InvalidArgumentException('Список содержит нечисловое значение.');
}
$total += $value;
$reporter->countSuccess();
$reporter->advance();
}
$reporter->setResult(['total' => $total]);
}
}В предметном обработчике вызывайте сервис своего пакета. Продлевайте аренду и проверяйте отмену во время долгой работы, а не только перед началом. Перед записью прогресса фиксируйте большие транзакции порциями: heartbeat внутри незавершённой транзакции не виден другим процессам. Не скрывайте ошибки за успешным возвратом из обработчика.
idempotent=true допустим только если повтор после неопределённого результата
безопасен. Для необратимых внешних действий оставьте false и одну попытку,
пока не реализована надёжная идемпотентность. Лимит на канал не заменяет
resourceKey для операций над общими данными и dedupKey/overlapPolicy
для повторной постановки. Эти правила задаются в
JobTypeDefinition, а не в транспортной очереди.
$run = Yii::$app->jobs->push('example.calculate-total', [
'values' => [10, 20, 30],
]);
if ($run !== null) {
$runId = $run->id;
}В payload передавайте JSON-совместимые значения и идентификаторы, а не модели,
соединения или PHP-замыкания. Канал выбирается определением типа. Постановка
через штатное общее DB-подключение участвует в транзакции приложения.
push() может вернуть null, когда политика пересечений пропустила дубль.
Не вставляйте записи прямо в cms_queue или cms_job_run.
Для повторения по расписанию используйте cms-agent с зарегистрированным
типом задания; расписание публикует запуск и не выполняет долгий обработчик
на web-запросе. Для запуска из UI задайте существующее RBAC-право типа задания
и проверьте доступ пользователя; произвольный маршрут не является правом.
Из корня установленного сайта:
php yii cms-job/worker/queues --json=1
php yii cms-job/worker/dispatchПервая команда показывает итоговую конфигурацию без потребления сообщений.
Вторая сама читает jobQueueFactory.queues и опрашивает все каналы, включая
пустые. Пустой канал добавляет проверку БД, но не отдельный ожидающий PHP-процесс.
В конфигурации по умолчанию php yii cms-job/worker без канала также запускает
диспетчер. Привязки к cms-hosting, домену, VPS или конкретному серверу нет.
Требуются Linux/PHP с pcntl, транспорт DbQueue и изоляция заданий.
Настройки проекта:
'components' => [
'jobWorker' => [
'mode' => 'dispatcher',
'maxProcesses' => 10,
'channelConcurrency' => 1,
'channels' => ['examples' => 2], // необязательное исключение
],
],Разные каналы могут выполняться параллельно. Пределы проверяются до резервирования сообщения. Ребёнок завершается после одного задания; обработка TTR, падения и токенов попыток общая с прежним воркером. В простое адаптер освобождает MySQL-соединение. Локальный lock исключает второй диспетчер этого сайта, но не ограничивает процессы на других серверах.
По умолчанию у каждого запуска стандартная карточка: статус, прогресс, события,
логи, отмена и повтор. Если типу нужен предметный отчёт (таблица этапов,
результаты по сайтам), укажите его в определении типа, а не подменяйте
контроллер cmsJob/admin-cms-job-run:
'jobRegistry' => [
'types' => [
'example.catalog.reconcile' => [
// ... handler, queue и прочие правила
'report' => \example\jobs\ReconcileReport::class,
],
],
],class ReconcileReport extends \skeeks\cms\job\reports\JobRunReport
{
public $view = '@example/views/job/reconcile';
public $permission = null; // право на отчёт сверх доступа к разделу
public function details(\skeeks\cms\job\models\CmsJobRun $run): array
{
return ReconcileProgress::collect($run); // только чтение, ограниченный объём
}
}Отчёт реализует JobRunReportInterface (canView, render, details).
Контроллер cms-job выводит его HTML над стандартной карточкой, поэтому
представление отчёта рисует только свою часть и не подключает карточку снова.
Представлению передаются model, details (начальные данные) и detailsUrl.
Страница обновляет отчёт опросом detailsUrl (progress с details=1) и
читает данные из data[<id>].report. Без details=1 опрос списков не
загружает подробности; подробности выбираются только для запусков текущего
сайта и только когда canView разрешает.
Ключ отчёта — тип задания, поэтому отчёты разных пакетов не конфликтуют.
Подмена контроллера через controllerMap из пакета недопустима: ключ один на
приложение, и yiisoft/config падает при одинаковом ключе в двух пакетах.
Диспетчер читает конфигурацию при запуске, без перечитывания PHP-конфига на лету. После регистрации нового канала или изменения лимитов:
- Убедитесь, что
worker/queues --json=1показывает актуальный канал и его типы. - Корректно остановите диспетчер через SIGTERM и запустите снова. Он прекратит
резервирование и дождётся текущих детей. Для systemd используйте службу,
настроенную с достаточным
TimeoutStopSecиKillMode=mixed. - Если службой управляет
cms-hostingс политикой «Все каналы через диспетчер», перезапуск организует автоматическая сверка. Она обнаруживает новые каналы, отключает прежнюю конфигурацию и после завершения заданий включает новую. Штатный интервал сверки — 5 минут; переход может занять несколько сверок и время завершения текущих заданий.
Параметр --queues=examples,maintenance ограничивает диспетчер указанными
каналами. Новая очередь вне этого списка не появится после простого рестарта:
нужно также обновить список запуска. Хостинг формирует и обновляет его по
конфигурации сайта, исключая свой служебный hosting-control.
Не удаляйте канал или тип до обработки/явной отмены старых сообщений и завершения активных запусков. Изменение конфига не переносит накопленные задания.
Прежние команды сохраняются:
php yii cms-job/worker --queue=maintenance
php yii cms-job/worker/cron --queue=maintenanceЯвный --queue всегда запускает отдельный воркер. jobWorker.mode=workers
отключает выбор диспетчера для команды без канала. Не запускайте старые воркеры
и диспетчер одновременно на одинаковых каналах, если нужны строгие лимиты.
Плановый --maxSeconds останавливает приём новых заданий и требует внешнего
менеджера процессов для повторного запуска; сам PHP-процесс себя не перезапускает.
Дальнейшая документация: