Режим воркера
Режим воркера оставляет процесс PHP живым между запросами: скрипт один раз поднимает приложение, а затем уходит в цикл, где на каждой итерации просит у Rapira очередной запрос. Старт выполняется один раз, при запуске, и каждый следующий запрос попадает в уже прогретое приложение. Состояние тоже живёт дольше запроса, поэтому скрипт воркера должен им управлять.
В классическом режиме входной скрипт, наоборот, выполняется с нуля на каждом запросе, и всё, что он успел построить, отбрасывается, как только на запрос дан ответ, поэтому поднять современный фреймворк — автозагрузчик, контейнер, конфигурацию, маршруты, соединения с базой — стоит одинаково на каждом запросе.
Режим воркера — это режим SAPI Worker; вместе с Classic он готов уже сегодня. Эта страница — руководство по программированию для него. Особый фреймворк режиму воркера не нужен: достаточно приложения, которое переживёт единственный запуск и множество запросов после него, а на это способно большинство современных фреймворков. Все четыре режима и то, от чего зависит доступный приложению режим, описывают Режимы выполнения, а руководства по конкретным фреймворкам собраны в разделе Фреймворки.
Резидентный цикл
Скрипт воркера состоит из трёх частей: всё, что вы поднимаете в начале файла, обработчик, отвечающий на один запрос, и цикл, который вызывает обработчик, пока сервер не остановится. Цикл написан на PHP и построен вокруг объекта-обработчика, который Rapira возвращает скрипту.
<?php
// worker.php
require __DIR__ . '/vendor/autoload.php';
use Rapira\Plugin\Http\HttpHandlerConfig;
use function Rapira\create_plugin_handler;
$http = create_plugin_handler(new HttpHandlerConfig());
$app = new App(); // booted once, reused for every request
$handler = static function () use ($app): void {
header('Content-Type: text/plain');
http_response_code(200);
echo $app->handle($_SERVER['REQUEST_URI']);
};
while ($http->handleRequest($handler)) {
gc_collect_cycles();
}Режим воркера у rapira serve включён по умолчанию, поэтому достаточно направить сервер на скрипт; классический режим включается явно:
rapira serve app/worker.phpОстальные флаги собраны в разделе Командная строка, а их аналоги для rapira.toml — в Конфигурации.
Что делает handleRequest()
handleRequest(callable $handler) — это весь контракт целиком:
- Блокируется до тех пор, пока воркеру не достанется запрос. Пока воркер ждёт в
handleRequest(), процессор он не расходует и при этом продолжает держать в памяти свой интерпретатор и поднятое приложение. - Заполняет суперглобальные переменные —
$_GET,$_POST,$_SERVER,$_COOKIEи всё остальное семейство — данными пришедшего запроса, заново и до того, как выполнится ваш обработчик. Обычный PHP-код, который их читает, работает точно так же, как под php-fpm. - Вызывает ваш обработчик без единого аргумента. Всё о запросе уже лежит в суперглобальных переменных, поэтому сигнатура колбэка —
function (): void. Всё остальное, что ему нужно — контейнер, приложение, логгер, — захватывайте черезuse. - Ответом становится ваш вывод.
echo,print,header(),http_response_code(),setcookie()— обработчик формирует ответ ровно так же, как классический скрипт. Как связаны данные запроса и вывод ответа, разбирает раздел HTTP. - Возвращает
true, когда запрос закончен, то есть «продолжаем», иfalse, когда сервер останавливается. На этом и держится цикл: как только пришлоfalse, выходите из него и дайте скрипту завершиться.
Получается, что запрос в режиме воркера — это одна итерация вашего цикла while. Rapira закрывает запрос вокруг обработчика: отрабатывают shutdown-функции и деструкторы, буферы вывода сбрасываются и обнуляются, сессия записывается и закрывается, а суперглобальные переменные наполняются заново к следующей итерации. Всё, что скрипт держит за пределами обработчика, при этом остаётся нетронутым.
Один обработчик на воркер
handleRequest() возвращает управление после каждого запроса, а не обслуживает их бесконечно, поэтому воркер живёт благодаря циклу вокруг вызова, и предоставить этот цикл должен сам скрипт воркера.
Значит, скрипт воркера обслуживает ровно один обработчик за раз. Если написать два цикла подряд, до второго дело не дойдёт, пока не завершится первый, а первый завершится только тогда, когда handleRequest() вернёт false, то есть когда сервер уже останавливается. Разводить запросы по разным веткам кода должен сам обработчик, внутри себя, а не несколько циклов.
while ($http->handleRequest($api)) {
}
// unreachable until shutdown
while ($http->handleRequest($web)) {
}Что живёт между запросами
Всё, что вы создаёте вне обработчика, живёт столько же, сколько процесс воркера: автозагрузчик, DI-контейнер, скомпилированные маршруты, конфигурация, открытые соединения с базой и кешем, прогретые кеши. Ничто из этого не пересоздаётся на каждом запросе.
Всё, что вы создаёте внутри обработчика, — обычная работа на один запрос: она освобождается, когда обработчик возвращает управление и запрос разбирается.
Где проходит граница между этими двумя частями — проектное решение скрипта воркера: состояние, задуманное общим, размещайте выше цикла, а состояние одного запроса оставляйте в обработчике или сбрасывайте до прихода следующего.
Общим оказывается и глобальное состояние, планировали вы это или нет: статические свойства, синглтоны, реестры, которые библиотека лениво наполняет по ходу дела, ini_set(), который так и не отменили. Под php-fpm всё это жило ровно один запрос, потому что завершение запроса в PHP сбрасывало и статику, и глобальные переменные, и ini_set(). Воркер Rapira этот сброс между запросами намеренно пропускает, так что они сохраняются. Приложение, которое не может отказаться от глобального состояния, запускается в классическом режиме: классический режим лишает вас прогретого приложения, которое воркер держит в памяти, но остаётся заменой php-fpm без единой правки, а перейти на воркер приложение сможет позже, когда состояние будет распутано.
Выбор плагина
create_plugin_handler() принимает объект конфигурации, и плагин выбирается по классу этого объекта. HttpHandlerConfig означает, что этот воркер обслуживает HTTP, и в ответ вы получаете HttpHandler.
Исключение Rapira\RapiraException она бросает в двух случаях: когда переданному классу конфигурации не соответствует ни один плагин и когда скрипт вообще запущен не в режиме воркера — в классическом режиме резидентного цикла нет, так что обработчику там осталось бы только сообщать об остановке.
Конфигурация ещё и описывает, к чему она относится: в $http->config->info лежит Rapira\PluginInfo с полями name и description (для HTTP-плагина это http и HTTP request handler):
$http = create_plugin_handler(new HttpHandlerConfig());
echo $http->config->info->name; // http
echo $http->config->info->description; // HTTP request handlerКак следить за воркером через getInfo()
$http->getInfo() возвращает Rapira\Plugin\Http\RuntimeInfo — счётчики самого этого воркера, снятые в момент вызова:
| Поле | Что это |
|---|---|
state | starting, idle, active, draining или free — см. ниже |
pid | Идентификатор процесса этого воркера |
queued | Сколько запросов прямо сейчас ждёт в очереди этого воркера |
handled | Сколько запросов воркер довёл до конца |
errors | Сколько из них закончились ошибкой |
recycles | Сколько раз воркеру пришлось пересобирать состояние после аварийного выхода PHP |
restarts | Сколько раз пришлось пересоздавать сам PHP-поток воркера |
Пять состояний описывают, где воркер находится в своём жизненном цикле: starting — мастер-процесс его форкнул, а он ещё не отчитался; idle — стоит и ждёт запроса, считаясь свободным резервом; active — обрабатывает запрос; draining — он уже на выходе (исчерпал квоту запросов или был помечен нездоровым) и в резерв больше не входит; free — за слотом вообще не закреплён воркер.
Обратите внимание: queued — это текущая глубина очереди, а не накопленный итог, и все счётчики относятся только к этому процессу. Они обнуляются при старте воркера, поэтому пришедший на замену воркер считает с нуля заново.
На этих счётчиках можно построить небольшой эндпоинт со статусом:
$handler = static function () use ($http): void {
$info = $http->getInfo();
header('Content-Type: application/json');
echo json_encode([
'pid' => $info->pid,
'state' => $info->state,
'queued' => $info->queued,
'handled' => $info->handled,
'errors' => $info->errors,
]);
};Подводные камни
Состояние протекает между запросами. Приложение, которое ведёт себя странно в воркере, но не под php-fpm, обычно протекает состоянием между запросами. Растущий статический массив, объект запроса, осевший в синглтоне, логгер, который держит контекст прошлого пользователя, — каждая такая мелочь проявляется только на втором запросе. Прибирайтесь явно, в начале или в конце обработчика, и сбрасывайте всё, что оставила после себя библиотека. pool.max_requests заставляет воркер завершиться после N запросов, чтобы мастер-процесс заменил его свежим: это ограничивает ущерб от медленной утечки, но не устраняет её.
Несобранные циклические ссылки. Подсчёт ссылок в PHP освобождает почти всё сразу, но циклические ссылки достаются сборщику циклов и ждут его запуска. Вызов gc_collect_cycles() на каждой итерации цикла — как в скрипте выше — не обязателен, но собирает их в предсказуемый момент: между запросами, а не посреди одного из них.
Запросы, которые никогда не заканчиваются. Воркер, застрявший в зависшем запросе, остаётся в нём сколь угодно долго и всё это время больше ничего не обрабатывает. pool.request_terminate_timeout_secs задаёт предел по реальному времени на один запрос и убивает воркер, который в него не уложился. Этот ключ и pool.max_requests описаны в Конфигурации, а что делает мастер-процесс, когда воркер умирает, рассказывает Модель процессов.
Непойманное исключение затрагивает запрос, а не воркер. Непойманное исключение в обработчике попадает в счётчик errors, а клиент получает 500 — если, конечно, обработчик не успел зафиксировать статус до того, как бросил. В любом случае цикл продолжается, так что исключение не уносит воркер с собой. С фатальной ошибкой иначе: она сворачивает резидентный скрипт, поэтому воркер выполняет его заново с самого начала и снова поднимает приложение. Именно это и считает счётчик recycles.
Работа после ответа. Если нужно отдать ответ и продолжить работу — разобрать очередь, записать событие в аудит, — ровно это и делает rapira_finish_request(). Она описана на странице HTTP.
Стаб-файл для IDE
Все классы и функции, которые Rapira отдаёт в PHP, объявлены в crates/php_sys/rapira.stub.php. Это авторитетное описание API — сигнатуры, типы свойств, назначение каждого класса, — и одновременно стаб для IDE: положите его в проект, и редактор начнёт подсказывать create_plugin_handler(), handleRequest() и всё остальное вместо того, чтобы подчёркивать их как неизвестные.