Skip to content

Режим Worker ​

Режим Worker сохраняет процесс PHP между запросами. Скрипт инициализирует приложение один раз, а затем ожидает запросы в цикле. Состояние приложения остаётся в памяти, поэтому скрипт воркера должен им управлять.

В режиме Classic входной скрипт каждый раз выполняется в новом запросе PHP, и Rapira удаляет состояние приложения после ответа. Это состояние включает автозагрузчик, контейнер, конфигурацию, маршруты и соединения с базой данных.

Режим Worker не требует определённого фреймворка. Он требует приложение, которое может обрабатывать много запросов после одной инициализации. Выбор режима описан в разделе Режимы выполнения. Руководства по фреймворкам находятся в разделе Фреймворки.

Резидентный цикл ​

Скрипт инициализирует приложение и определяет обработчик одного запроса. Затем он вызывает \Rapira\handle_request() в цикле до остановки воркера.

php
<?php
// worker.php
require __DIR__ . '/vendor/autoload.php';

$app = new App(); // The worker creates this object once and reuses it.

$handler = static function () use ($app): void {
    header('Content-Type: text/plain');
    http_response_code(200);
    echo $app->handle($_SERVER['REQUEST_URI']);
};

while (\Rapira\handle_request($handler)) {
    gc_collect_cycles();
}

Режим по умолчанию - Dispatcher. Выберите режим Worker ключом mode = "worker" в таблице [http.pool] файла rapira.toml:

toml
[http]
listen = "127.0.0.1:8000"

[http.pool]
entrypoint = "app/worker.php"
mode = "worker"
bash
rapira serve rapira.toml

Остальные ключи собраны в разделе Конфигурация.

Контракт handle_request() ​

\Rapira\handle_request(callable $handler): bool имеет следующий контракт:

  • Ожидает запрос для этого воркера. Во время ожидания воркер не использует процессор.
  • Заполняет данные запроса в $_GET, $_POST, $_SERVER, $_COOKIE, $_FILES и $_REQUEST до запуска обработчика. Код читает эти суперглобальные переменные так же, как под php-fpm.
  • Вызывает обработчик без аргументов. Используйте сигнатуру function (): void. Захватите зависимости, например контейнер или логгер, с помощью use. Rapira игнорирует возвращаемое значение.
  • Вывод обработчика является ответом. Обработчик может использовать echo, print, header(), http_response_code() и setcookie(). Обработка запросов и ответов описана в разделе HTTP.
  • Возвращает true после каждого запроса. Функция возвращает false, когда воркер начинает остановку. Завершите цикл и скрипт после возврата false.
  • Вызывайте функцию только из цикла верхнего уровня скрипта. Вызов изнутри обработчика бросает \Error. Не вызывайте функцию из shutdown-функции или деструктора.

Один запрос в режиме Worker соответствует одной итерации цикла while. Перед каждым вызовом обработчика Rapira заново заполняет суперглобальные переменные. После вызова Rapira запускает shutdown-функции запроса, сбрасывает буферы вывода и закрывает сессию. Значения, которые скрипт хранит вне обработчика, остаются в памяти.

До первого вызова handle_request() $_SERVER содержит окружение процесса и путь входного скрипта, как под PHP CLI. Полный список приведён в разделе Режимы выполнения.

Один цикл на воркер ​

Скрипт воркера выполняет один цикл с одним обработчиком. В примере ниже второй цикл начинается только после завершения первого цикла. Первый цикл завершается только при остановке. Используйте один обработчик, который распределяет все запросы.

php
while (\Rapira\handle_request($api)) {
}

// Code reaches this loop only during shutdown.
while (\Rapira\handle_request($web)) {
}

Состояние, которое остаётся между запросами ​

Объекты, созданные вне обработчика, существуют до завершения цикла воркера. К ним относятся автозагрузчик, контейнер, маршруты, конфигурация, открытые соединения и кешированные данные. Rapira не создаёт это состояние для каждого запроса.

Значения, созданные внутри обработчика, принадлежат одному запросу. PHP освобождает их после возврата обработчика и удаления последних ссылок на них.

Скрипт воркера определяет время жизни состояния. Размещайте состояние приложения перед циклом. Размещайте состояние запроса в обработчике или сбрасывайте его перед следующим запросом.

Глобальное состояние также сохраняется между запросами. К нему относятся статические свойства, синглтоны, реестры и изменения ini_set(). php-fpm сбрасывает эти значения в конце каждого запроса. Воркер Rapira их сохраняет.

Используйте режим Classic, если приложение не может сбросить глобальное состояние. Режим Classic является совместимой заменой php-fpm. Выберите режим Worker после исправления глобального состояния.

Shutdown-функции ​

Цикл воркера - это один запуск скрипта воркера, от инициализации до конца скрипта. PHP выполняет каждую shutdown-функцию, которую регистрирует код инициализации, один раз в конце цикла. PHP выполняет каждую shutdown-функцию, которую регистрирует обработчик, один раз в конце этого запроса.

Регистрируйте очистку ресурсов процесса при инициализации. Регистрируйте очистку ресурсов запроса внутри обработчика.

php
register_shutdown_function(static function (): void {
    // Runs once when the worker cycle ends.
});

$handler = static function (): void {
    register_shutdown_function(static function (): void {
        // Runs at the end of this request.
    });
};

while (\Rapira\handle_request($handler)) {
}

В конце цикла первыми выполняются функции, зарегистрированные при инициализации, в порядке регистрации. Функция, зарегистрированная после цикла, выполняется после них.

Для объектов действует другое правило. Rapira не запускает все деструкторы в конце запроса. PHP уничтожает объект после удаления последней ссылки на него. После возврата из обработчика PHP уничтожает созданные в нём объекты только тогда, когда на них не осталось ссылок. Глобальный объект, созданный при инициализации, остаётся между запросами. Его метод __destruct() выполняется один раз при завершении цикла.

Почему shutdown-функция инициализации не выполняется после первого запроса?

PHP хранит shutdown-функции в состоянии запроса. При завершении запроса PHP вызывает функции и освобождает список. При первом вызове handle_request() Rapira удаляет и сохраняет регистрации инициализации, поэтому каждый запрос содержит только свои регистрации. В конце цикла Rapira восстанавливает сохранённый список и добавляет регистрации, созданные после цикла.

Только в режиме Worker ​

Функции handle_request() нужен резидентный цикл, а он есть только в режиме Worker. В режимах Classic и Dispatcher функция бросает Rapira\Exception\NotInWorkerModeError. Все классы исключений Rapira реализуют маркерный интерфейс Rapira\Exception\RapiraThrowable. Некоторые ошибки использования являются обычными \Error или \ValueError, и catch для RapiraThrowable их не перехватывает. Пример: вызов handle_request() внутри его обработчика.

Rapira\get_mode() возвращает режим текущего процесса как вариант перечисления Rapira\Mode. Скрипт, который работает больше чем в одном режиме, читает его до входа в цикл:

php
if (\Rapira\get_mode() === \Rapira\Mode::Worker) {
    while (\Rapira\handle_request($handler)) {
    }
}

Типичные проблемы ​

Состояние запроса остаётся между запросами. Если приложение не работает только в режиме Worker, найдите состояние запроса, которое остаётся. Примеры: растущий статический массив, объект запроса в синглтоне или старые данные пользователя в логгере.

Сбрасывайте это состояние в начале или конце обработчика. Также сбрасывайте состояние запроса в библиотеках. http.pool.max_requests заменяет воркер, когда он обслужил больше заданного числа запросов. Это ограничивает последствия утечки памяти, но не исправляет её.

Несобранные циклические ссылки. Подсчёт ссылок PHP сразу освобождает большинство значений. Циклические ссылки он освобождает только при запуске сборщика циклов. Пример вызывает gc_collect_cycles() между запросами. Этот вызов не обязателен, но делает время сборки предсказуемым.

Незавершённые запросы. Воркер не может обрабатывать другой запрос во время выполнения текущего запроса. http.pool.request_terminate_timeout_secs ограничивает фактическое время одного запроса. Когда запрос превышает этот предел, Rapira останавливает воркер и запускает новый. Этот ключ и http.pool.max_requests описаны в разделе Конфигурация. Последовательность остановки описана в разделе Модель процессов.

Инициализация завершается с ошибкой. Скрипт воркера должен вызвать handle_request() и получить запрос. Непойманное исключение при инициализации может завершить скрипт до этого. Rapira считает это неудачным запуском. Затем воркер ожидает запрос до 5 секунд, отвечает на него 503 и снова запускает скрипт.

После пяти неудачных запусков подряд Rapira записывает в лог worker keeps failing to boot; flagged unhealthy, и воркер завершается. Мастер запускает новый воркер после задержки. Если при старте сервера ни один воркер пула не запустился и не обслужил запрос, сервер вместо этого останавливается с кодом завершения 70. Контроль воркеров описан в разделе Модель процессов.

Непойманное исключение затрагивает один запрос, а не воркер. Если обработчик бросает исключение, Rapira сначала вызывает функцию, которую зарегистрировал set_exception_handler(). Если никакая функция не обработала исключение, Rapira возвращает 500, если обработчик ещё не отправил заголовок ответа. В обоих случаях цикл продолжается. Вызов exit() или die() в обработчике завершает только текущий запрос, и Rapira отправляет вывод как ответ. Фатальная ошибка завершает скрипт воркера, и Rapira снова запускает скрипт с новой инициализацией.

Работа после ответа. rapira_finish_request() отправляет ответ до завершения обработчика. Затем обработчик может выполнить дополнительную работу, например записать событие аудита. Дополнительная информация находится на странице HTTP.

Стаб-файлы для IDE ​

Rapira объявляет свои функции и классы PHP в стаб-файлах в каталогах crates/sapi и crates/plugins. API воркера находится в rapira.stub.php. Общие классы исключений находятся в rapira_exception.stub.php. Эти файлы объявляют сигнатуры, типы свойств и назначение классов. Добавьте их в проект, чтобы включить автодополнение в IDE для \Rapira\handle_request(), \Rapira\get_mode() и других API.