### Введение
Очереди (queues) это одна из сильных сторон фреймворка. Хотя они реализованы так, чтобы любой ремесленник смог их осилить, всё же некоторый порог для входа есть и моя цель помочь вам его преодолеть.
Закончив читать эту статью вы научитесь:
* Использовать очереди для того чтобы быстро выполнять http-запросы.
* Запускать обработку очереди с минимальными (нулевыми) дополнительными требованиями к серверу.
* Обеспечить постоянную работу очереди.
Здесь будет рассмотрен только самый минималистический вариант организации очередей: через драйвер _database_. Будем считать, что это local или staging окружение, на котором не ожидается больших нагрузок. Хотя, если честно, большинство "рабочих" окружений также нельзя назвать highload :) Надо с чего-то начинать, начните с простого.
Подразумевается, что вы используете свежую версию Laravel, сейчас это v6.14, но наверное написанное подойдёт для любой версии 5.0+. Также я не делаю никаких предположений или рекомендаций по поводу вашего девелоперского стека: WAMP, Valet, какая-нибудь реализация в контейнере и т.д. Важно чтобы у вас был рабочий веб-сервер и командная строка.
### 1\. Типичная проблема: отправка почты
Будем считать, что подключать почту в Laravel вы умеете. То есть настройка демо-аккаунта в [mailtrap.io](https://medium.com/@christianjombo/setting-up-mailtrap-for-laravel-development-313133bb800c) или [реального SMTP](https://medium.com/@agavitalis/how-to-send-an-email-in-laravel-using-gmail-smtp-server-53d962f01a0c) останется за рамками статьи. Если вы споткнулись уже на этом шаге, я советую отложить очереди на потом. Это не тот случай, когда вы можете перепрыгивать через ступеньки!
Начнём немного издалека. Для наших нужд вам понадобится тестовый маршрут и его обработчик в контроллере.
В _routes/web.php_ добавьте такую запись:
```php
Route::get('email', 'EmailController@sendEmail');
```
Создадим контроллер с нужным методом. В командной строке выполните
```bash
php artisan make:controller EmailController
```
Откройте новый файл _app/Http/Controllers/EmailController.php_ и сделайте так:
```php
send(new SendMailable());
$spent = microtime(true) - $ts;
echo 'Email sent. Time spent ' . sprintf('%.4f sec', $spent);
}
}
```
Я захардкодил email-адрес, т.к. тестирую на mailtrap и получаю туда всю почту. Вы можете использовать что-то более реальное, если надо.
Вы можете заметить, что я замеряю время выполнения с высокой точностью. Если вы достаточно внимательны, вы увидели ещё и класс для формирования письма. Пока его нет, создадим и его тоже.
```bash
php artisan make:mail SendMailable
```
Откройте новый файл _app/Mail/SendMailable.php_ и отредактируйте так:
```php
view('welcome');
}
}
```
Вместо 'welcome' можете использовать любой шаблон, не требующий входящих данных.
Попробуйте открыть адрес _site.test/email_ (замените домен на ваш) в браузере. Вы должны увидеть примерно такое:
> Email sent. Time spent 2.5007 sec
И через небольшое время вы получите письмо на mailtrap или реальный ящик.
**2.5 секунды это слишком много для выдачи странички!** А ведь мы не делали ничего, кроме отправки одного письма. В этом и состоит проблема — обращения server-to-server типа отправки почты занимают слишком много времени, чтобы выполнять их неспостредственно из контроллера.
Будем решать проблему с помощью очередей.
### 2\. Очередь - первые шаги
Вместо обращения к фасаду Mail в контроллере мы будем создавать задание для очереди, которое делает в точности ту же работу, но потом :) Нам надо вернуть ответ в браузер как можно быстрее. Тяжёлую работу пусть делает воркер очереди.
Новый вариант обработчика запроса в _app/Http/Controllers/EmailController.php_:
```php
delay(now()->addSeconds(3));
dispatch($emailJob);
$spent = microtime(true) - $ts;
echo 'Email sent. Time spent ' . sprintf('%.4f sec', $spent);
}
}
```
Не забудте поправить секцию use, иначе получите ошибку! Как видите у нас появляется ещё один класс — задание. Создадим его через artisan:
```bash
php artisan make:job SendEmailJob
```
И отредактируем новый файл _app/Jobs/SendEmailJob.php_
```php
send(new SendMailable());
}
}
```
Как видите Mail::to() переехал сюда. Теперь он должен выполняться не во время ответа на HTTP запрос, а когда-то потом.
Уже нетерпится поробовать! Снова открываем адрес _site.test/email_ в браузере. Получаем примерно такое:
>
> Email sent. Time spent 2.5102 sec
>
Омагад! Омагад! Время если и изменилось, то в пределах стат. погрешности. Где же обещанный профит, спросите вы. И я вам таки отвечу: не спешите, работа ещё не закончена. Забегая вперёд, нам понадобится заменить используемый драйвер очереди. По умолчанию он равен значению "sync". Драйвер очередей _sync_ реализует псевдо-очередь, которая выполняется непосредственно в момент создания.
На самом деле здесь таится великая мудрость! Laravel разделяет код и конфигурацию. Вы пишете код так, чтобы он выполнялся эффективно при наличии спецсредств, но он будет работать без ошибок и в отстутствии этих средств, только менее эффективно.
Итак, мы добрались до самого важного:
### 3\. Драйвер очереди database
Laravel "из коробки" содержит драйвера для работы с несколькими инструментами очередей, вы можете ознакомиться с деталями в [официальной документации](https://laravel.com/docs/master/queues#driver-prerequisites). Но одного только драйвера недостаточно. В большинстве случаев, вам понадобится отдельный сервер очередей. Как я обещал, я опишу самый доступный вариант: драйвер _database_.
Преимущество драйвера _database_ в том, что дополнительных серверов не понадобится. И при этом очередь будет реально работать параллельно с вашими веб-запросами!
Прежде чем использовать драйвер _database_ мы должны создать необходимые таблицы в базе. А ещё раньше описать параметры доступа к базе в .env. Создание самой БД и подключение к ней выходят за рамки статьи.
Для создания нужных таблиц выполните такие команды из консоли:
```bash
php artisan queue:table
php artisan migrate
```
Теперь у нас есть таблицы. В файле _.env_ укажем название драйвера:
```dotenv
QUEUE_CONNECTION=database
```
И снова обратимся к странице _site.test/email_ в браузере. Получаем примерно такое:
>
> Email sent. Time spent 0.0193 sec
>
Время выполнения изменилось с секунд до сотых долей секунды. В вашем окружении результат может быть лучше или хуже, но соотношение "до" и "после" будет таким же поразительным.
Что реально произошло сейчас: было создано задание (работоспособность мы уже протестировали выше), задание было помещено в очередь в таблице `jobs`, можете его там найти через phpmyadmin или что-вы-там-используете. И на этом пока всё! :) Очередь пока только копится. Чтобы она начала выполняться, в командной строке запустите:
```bash
php artisan queue:work
[2020-02-09 13:40:43][15] Processing: App\Jobs\SendEmailJob
[2020-02-09 13:40:45][15] Processed: App\Jobs\SendEmailJob
```
— на каждое задание в очереди будут сообщения "Processing" и "Processed". Ну или вместо "Processed" будет сообщение об ошибке, если вы что-то упустили. При ошибке задание из таблицы `jobs` перемещается в таблицу `failed_jobs`. У вас будет возможность исправить причину ошибки и попробовать заново поместить задание в очередь выполнения командой `php artisan queue:retry 5` где 5 это идентификатор задания. У вас будет свой! Или забыть про неудачное задание командой `php artisan queue:forget 5`
Подробности вы найдёте в [документации](https://laravel.com/docs/master/queues). Последний важный момент, который я вам расскажу это как заставить очередь обрабатываться регулярно. Так чтобы один раз настроить и забыть. А оно бы работало.
### 4\. "Эта музыка будет вечной, если я заменю батарейки"
Вы наверное заметили, что команда `php artisan queue:work` не закончилась после того как все задания в очереди выполнились. Она ждёт новых заданий! Вы можете прервать выполнение нажав Ctrl+C.
Вы можете указать чтобы ожидания новых заданий не было, запустив воркер вот так:
```bash
php artisan queue:work --stop-when-empty
```
Или так:
```bash
php artisan queue:work --once
```
Чтобы обработать только одно задание из очереди. Если очередь пуста, воркер завершается без ожидания.
Открою маленький секрет: воркер **надо** останавливать хотябы иногда. На то есть две причины:
1. Воркер не замечает изменений в коде приложения. Он работает с тем кодом, который получил в момент запуска. Но вы же программисты и постоянно что-то меняете :) Поэтому перезапуск нужен.
2. На сегодняшний день PHP ещё неидеально подходит для создания постоянно работающих демонов. Есть проблемы с утечкой памяти. Ситуация улучшается с новыми версиями, но пока совсем не исправилась. Поэтому воркер может забирать всё больше и больше памяти, пока не исчерпает всю доступную.
Итак, с одной стороны мы хотим чтобы очередь выполнялась постоянно. С другой, нам надо останавливаться. Казалось бы есть противоречие. Стандартно проблему слежения за наличием демона и перезапуск его при нужных условиях обеспечивают с помощью supervisor. Есть хорошие статьи про это, например [вот эта на Medium.com](https://medium.com/@rohit_shirke/configuring-supervisor-for-laravel-queues-81e555e550c6). Но я обещал вам показать как попроще и без лишних зависимостей. Получите:
Самый простой способ иметь рабочую очередь — это запускать воркер периодически с выходом при пустой очереди. Через CRON!
Поступаем по тому же принципу как с выбором драйвера: "этот инструмент нам всё равно нужен, так что просто добавим в него новую функцию".
Ваш crontab может выглядеть так:
```crontab
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
*/2 * * * * cd /path-to-your-project && php artisan queue:work --stop-when-empty >> /dev/null 2>&1
```
Надо только подставить сюда путь к реальному приложению Laravel. Я указал запускать расписание Laravel каждую минуту, а воркер [каждые две минуты](https://crontab.guru/every-2-minutes).
Буду рад обсудить разные варианты в коментариях.
Да прибудет с вами Сила!