Фоновые компоненты

Состав приложения

Приложение Winter — это не обязательно веб-сервер. Оно собирается из компонентов, и веб среди них лишь один из четырёх: рядом могут работать долгоживущие процессы, флоты воркеров под супервизией и планировщик задач. Любой из них может быть единственным.

Манифест атрибуты #[Enable*]Запуск php call runБез веба да

Что такое компоненты и зачем

Компонент — самостоятельная часть приложения со своим способом работы: веб отвечает на запросы, процесс крутится в цикле, демон держит флот воркеров, планировщик запускает задачи по времени.

Проблема. У приложения редко бывает одна форма. Сервису с API почти всегда нужен кто-то ещё: разгребать очередь, опрашивать оборудование, чистить старые записи по ночам. Обычно это решается снаружи — отдельными скриптами под cron, юнитами systemd, сторонним супервизором. И тогда одно приложение превращается в несколько развёртываний: разные способы запуска, разные конфигурации, разные логи, а общий код между ними приходится тащить пакетом.

Решение. Winter описывает состав приложения в самом приложении. Класс-точка входа перечисляет атрибутами, из чего оно состоит, — и php call run поднимает всё это вместе, под одним конфигом, с общим контейнером зависимостей и общими логами. Понадобился воркер — это строка в манифесте, а не новое развёртывание.

Манифест

Всё объявляется атрибутами на классе приложения:

bootstrap.php
<?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] нет

Повторяемость означает буквально это: несколько разных процессов и несколько разных демонов объявляются несколькими строками.

php
#[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

Запуск

Одна команда поднимает всё, что перечислено в манифесте:

bash
php call run       # рабочий режим
php call run dev   # разработка: перезапуск при изменении .php

Когда в манифесте есть веб, главным процессом становится HTTP-сервер, а остальные компоненты запускаются рядом и находятся под присмотром того же мастера: упал воркер — его поднимут, остановили приложение — остановятся все.

Приложение без веба

Убираете #[EnableWeb] — и приложение поднимается безголовым: только процессы, демоны и планировщик.

bootstrap.php
#[EnableScheduler]
#[EnableDaemon(\Main\Process\Emails::class)]
final class Application extends WinterApplication { /* ... */ }

Такое приложение по-прежнему пользуется контейнером, конфигурацией, логированием и доступом к базе — просто не слушает порт. Это обычный способ развернуть отдельный обработчик очереди или ночной регламент рядом с основным сервисом.

Пустой манифест — ошибка

Хотя бы один компонент объявить нужно. Приложение без единого #[Enable*] не запустится и скажет об этом прямо: No components declared со списком доступных атрибутов.

Что выбрать

Четыре инструмента легко перепутать, потому что все они «делают что-то в фоне». Разводятся они по вопросу «когда это должно работать».

Нужно Инструмент
Выполнить кусок работы, не задерживая ответ клиенту #[Async]
Крутить один бесконечный цикл: слушать очередь, опрашивать устройство Процесс
То же самое, но в несколько рук, с перезапуском упавших Демон
Запускать по расписанию: каждые 5 минут, каждую ночь в 3:00 Планировщик

Разберём границы, потому что именно на них ошибаются:

#[Async] — не фоновая задача. Метод уходит в отдельную корутину и чередуется с вызывающим кодом на ожиданиях ввода-вывода, но работа остаётся внутри того же воркера и живёт ровно столько, сколько он: перезапуск её потеряет, на другую машину она не уедет, очереди у неё нет. Годится, чтобы не заставлять клиента ждать; не годится, чтобы гарантированно доставить письмо — для этого нужен процесс или демон с очередью.

Процесс против демона. Процесс — один экземпляр, демон — несколько под супервизией, с политикой перезапуска и масштабированием. Если работа делится на независимые единицы и одного воркера мало, берите демона; если задача по своей природе одна (слушать один сокет, держать одно подключение), берите процесс.

Планировщик против процесса с sleep(). Цикл с sleep(300) внутри процесса кажется простым решением, но с ним вы сами отвечаете за то, чтобы задача не наложилась сама на себя, за пропуски после перезапуска и за расписание в терминах времени суток. Планировщик всё это уже умеет.

Дальше