Состав приложения
Приложение Winter — это не обязательно веб-сервер. Оно собирается из компонентов, и веб среди них лишь один из четырёх: рядом могут работать долгоживущие процессы, флоты воркеров под супервизией и планировщик задач. Любой из них может быть единственным.
Что такое компоненты и зачем
Компонент — самостоятельная часть приложения со своим способом работы: веб отвечает на запросы, процесс крутится в цикле, демон держит флот воркеров, планировщик запускает задачи по времени.
Проблема. У приложения редко бывает одна форма. Сервису с API почти всегда
нужен кто-то ещё: разгребать очередь, опрашивать оборудование, чистить старые
записи по ночам. Обычно это решается снаружи — отдельными скриптами под cron,
юнитами systemd, сторонним супервизором. И тогда одно приложение превращается в
несколько развёртываний: разные способы запуска, разные конфигурации, разные логи,
а общий код между ними приходится тащить пакетом.
Решение. Winter описывает состав приложения в самом приложении. Класс-точка
входа перечисляет атрибутами, из чего оно состоит, — и php call run поднимает
всё это вместе, под одним конфигом, с общим контейнером зависимостей и общими
логами. Понадобился воркер — это строка в манифесте, а не новое развёртывание.
Манифест
Всё объявляется атрибутами на классе приложения:
<?php
use Flytachi\Winter\Kernel\App\Attribute\EnableDaemon;
use Flytachi\Winter\Kernel\App\Attribute\EnableProcess;
use Flytachi\Winter\Kernel\App\Attribute\EnableScheduler;
use Flytachi\Winter\Kernel\App\Attribute\EnableWeb;
use Flytachi\Winter\Kernel\WinterApplication;
require __DIR__ . '/vendor/autoload.php';
#[EnableWeb] // HTTP-сервер
#[EnableScheduler] // задачи по расписанию
#[EnableProcess(\Main\Process\SnmpPoller::class)] // один воркер
#[EnableDaemon(\Main\Process\Emails::class)] // флот воркеров
final class Application extends WinterApplication
{
public static function main(array $argv): never
{
parent::run($argv);
}
}Это весь файл запуска. Хуков, которые нужно переопределять, нет: настройки живут в обычных классах, которые находит сканер, — см. Внедрение зависимостей и Настройку веб-слоя.
Четыре компонента
| Атрибут | Что поднимает | Повторяемый |
|---|---|---|
#[EnableWeb] |
HTTP-сервер | нет |
#[EnableProcess(Класс)] |
Один долгоживущий процесс | да |
#[EnableDaemon(Класс)] |
Флот воркеров под супервизией | да |
#[EnableScheduler] |
Планировщик задач #[Scheduled] |
нет |
Повторяемость означает буквально это: несколько разных процессов и несколько разных демонов объявляются несколькими строками.
#[EnableProcess(\Main\Process\SnmpPoller::class)]
#[EnableProcess(\Main\Process\Heartbeat::class)]
#[EnableDaemon(\Main\Process\Emails::class)]
#[EnableDaemon(\Main\Process\Webhooks::class)]#[EnableWeb] не принимает ни адреса, ни порта — это свойство развёртывания, а не
приложения. Адрес задаётся флагами --host/--port, переменными окружения или
конфигуратором веб-слоя.
Что компонентом не является
Два атрибута выглядят похоже, но ничего не поднимают:
| Атрибут | Что делает |
|---|---|
#[EnableAsync] |
Включает проксирование методов с #[Async] — см. Асинхронные вызовы |
#[EnableActuator] |
Добавляет вебу диагностические эндпоинты — см. Actuator / Health |
Запуск
Одна команда поднимает всё, что перечислено в манифесте:
php call run # рабочий режим
php call run dev # разработка: перезапуск при изменении .phpКогда в манифесте есть веб, главным процессом становится HTTP-сервер, а остальные компоненты запускаются рядом и находятся под присмотром того же мастера: упал воркер — его поднимут, остановили приложение — остановятся все.
Приложение без веба
Убираете #[EnableWeb] — и приложение поднимается безголовым: только процессы,
демоны и планировщик.
#[EnableScheduler]
#[EnableDaemon(\Main\Process\Emails::class)]
final class Application extends WinterApplication { /* ... */ }Такое приложение по-прежнему пользуется контейнером, конфигурацией, логированием и доступом к базе — просто не слушает порт. Это обычный способ развернуть отдельный обработчик очереди или ночной регламент рядом с основным сервисом.
Пустой манифест — ошибка
Хотя бы один компонент объявить нужно. Приложение без единого #[Enable*] не
запустится и скажет об этом прямо: No components declared со списком доступных
атрибутов.
Что выбрать
Четыре инструмента легко перепутать, потому что все они «делают что-то в фоне». Разводятся они по вопросу «когда это должно работать».
| Нужно | Инструмент |
|---|---|
| Выполнить кусок работы, не задерживая ответ клиенту | #[Async] |
| Крутить один бесконечный цикл: слушать очередь, опрашивать устройство | Процесс |
| То же самое, но в несколько рук, с перезапуском упавших | Демон |
| Запускать по расписанию: каждые 5 минут, каждую ночь в 3:00 | Планировщик |
Разберём границы, потому что именно на них ошибаются:
#[Async] — не фоновая задача. Метод уходит в отдельную корутину и чередуется
с вызывающим кодом на ожиданиях ввода-вывода, но работа остаётся внутри того же
воркера и живёт ровно столько, сколько он: перезапуск её потеряет, на другую
машину она не уедет, очереди у неё нет. Годится, чтобы не заставлять клиента ждать;
не годится, чтобы гарантированно доставить письмо — для этого нужен процесс или
демон с очередью.
Процесс против демона. Процесс — один экземпляр, демон — несколько под супервизией, с политикой перезапуска и масштабированием. Если работа делится на независимые единицы и одного воркера мало, берите демона; если задача по своей природе одна (слушать один сокет, держать одно подключение), берите процесс.
Планировщик против процесса с sleep(). Цикл с sleep(300) внутри процесса
кажется простым решением, но с ним вы сами отвечаете за то, чтобы задача не
наложилась сама на себя, за пропуски после перезапуска и за расписание в терминах
времени суток. Планировщик всё это уже умеет.
Дальше
- Процессы — один долгоживущий воркер
- Демоны — флот воркеров с супервизией
- Планировщик — задачи по расписанию
- Асинхронные вызовы — не заставлять клиента ждать медленную операцию
- Рантаймы — как это исполняется под Swoole