### Введение Очереди (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). Буду рад обсудить разные варианты в коментариях. Да прибудет с вами Сила!