Cron и очереди
Cron — планировщик Drupal: он периодически запускает служебную работу всех модулей — очистку, проверку обновлений, индексацию поиска. Очередь — список отложенных задач, которые выполняются понемногу в фоне. Вместе они позволяют не делать долгую работу, пока посетитель ждёт страницу.
Задача по расписанию
Свою работу в планировщик добавляет хук hook_cron в файле blog.module:
// web/modules/custom/blog/blog.module
/**
* Implements hook_cron().
*/
function blog_cron(): void
{
\Drupal::database()->delete('blog_subscribers')
->condition('created', strtotime('-2 years'), '<')
->execute();
\Drupal::logger('blog')->notice('Старые подписки удалены.');
}При каждом запуске планировщика функция удаляет подписки старше двух лет и пишет строку в журнал.
Запуск
vendor/bin/drush cr
vendor/bin/drush cron
vendor/bin/drush ws --count=5Вторая команда запускает планировщик вручную, третья показывает последние записи журнала — среди них будет «Старые подписки удалены.».
На рабочем сервере планировщик запускают по расписанию операционной системы, например каждые 15 минут:
*/15 * * * * cd /var/www/site && vendor/bin/drush cronСтроку добавляют в таблицу заданий сервера командой crontab -e. Без неё Drupal запускает cron сам, вместе с запросами посетителей, раз в три часа — это настраивается на странице «Конфигурация → Система → Cron».
Зачем очередь
Хук blog_cron() должен работать быстро: пока он выполняется, остальные модули ждут. Если нужно обработать тысячи записей — например, уведомить всех подписчиков о новой статье, — задачи складывают в очередь, а планировщик разбирает её частями.
Добавление в очередь
Положим задачу в очередь, когда создаётся статья:
// web/modules/custom/blog/blog.module
use Drupal\node\NodeInterface;
/**
* Implements hook_ENTITY_TYPE_insert() for node.
*/
function blog_node_insert(NodeInterface $node): void
{
if ($node->bundle() !== 'article') {
return;
}
$emails = \Drupal::database()->select('blog_subscribers', 's')
->fields('s', ['email'])
->execute()
->fetchCol();
$queue = \Drupal::queue('blog_notify');
foreach ($emails as $email) {
$queue->createItem(['email' => $email, 'nid' => $node->id()]);
}
}\Drupal::queue('blog_notify') возвращает очередь с таким именем, createItem() кладёт в неё одну задачу — любой массив данных. Сохранение статьи остаётся быстрым: письма здесь не отправляются.
Обработчик
Задачи выполняет плагин QueueWorker. Его папка — src/Plugin/QueueWorker, а id совпадает с именем очереди:
// web/modules/custom/blog/src/Plugin/QueueWorker/NotifyWorker.php
namespace Drupal\blog\Plugin\QueueWorker;
use Drupal\Core\Queue\Attribute\QueueWorker;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
#[QueueWorker(
id: 'blog_notify',
title: new TranslatableMarkup('Уведомления подписчикам'),
cron: ['time' => 30],
)]
class NotifyWorker extends QueueWorkerBase
{
public function processItem($data): void
{
\Drupal::logger('blog')->notice('Уведомление для @email о статье @nid.', [
'@email' => $data['email'],
'@nid' => $data['nid'],
]);
}
}processItem() получает одну задачу. Здесь он только пишет в журнал — на этом месте была бы отправка письма. Запись cron: ['time' => 30] разрешает планировщику тратить на эту очередь до 30 секунд за запуск; оставшиеся задачи дождутся следующего.
Проверка
vendor/bin/drush cr
vendor/bin/drush queue:list
vendor/bin/drush queue:run blog_notifyСоздайте статью, затем выполните команды. queue:list покажет очередь blog_notify и число задач в ней, queue:run выполнит их сразу, не дожидаясь cron.
Частая ошибка
Если processItem() бросает исключение, задача остаётся в очереди и будет повторена при следующем запуске. Это полезно при временном сбое, но задача с постоянной ошибкой — например, несуществующий адрес — будет повторяться бесконечно. Перехватывайте ожидаемые ошибки внутри обработчика и записывайте их в журнал, а исключение оставляйте для случаев, когда повтор действительно поможет.