Timer: расписание и границы блокировки
Модель и публичный API
Заголовок раздела «Модель и публичный API»Класс — localzet\Timer. Интервалы — float секунды; callback получает элементы args как позиционные аргументы. Таймер работает в выбранном event loop текущего процесса. Это не постоянная scheduled job и не координация workers.
| Метод | Поведение |
|---|---|
| init(?EventInterface $event = null): void | Явный loop, global loop Server либо новый Select |
| repeat(float $interval, callable $callback, array $args = []): int | ID повторяющегося таймера |
| delay(float $delay, callable $callback, array $args = []): int | ID одноразового таймера |
| add(float $interval, callable $callback, ?array $args = [], bool $persistent = true): int | Совместимый wrapper repeat/delay |
| del(int $id): bool | Удаление таймера; false, если регистрации нет |
| delAll(): void | Удаляет все таймеры loop, в том числе таймеры других частей приложения |
| getAll(): array | Диагностика ['count' => integer], не список определений |
| sleep(float $delay): void | Приостанавливает только с SuspensionCapableInterface backend; иначе блокирующий usleep |
| signalHandle(): void | Compatibility no-op; отдельного SIGALRM scheduler нет |
Отрицательный интервал вызывает RuntimeException. Для пользовательского ввода также отсекайте не-конечные числа и ограничивайте максимум: проверка только на отрицательное значение не является полной validation policy. Точность зависит от loop/ОС и нагрузки. Ноль не означает гарантированную частоту выполнения.
Полный запускаемый пример
Заголовок раздела «Полный запускаемый пример»Пример явно запускает Select loop без Server: проверены отмена, одноразовый callback с аргументом, остановка loop и диагностический count.
<?php
declare(strict_types=1);
use localzet\Timer;use localzet\Server\Events\Select;
require __DIR__ . '/vendor/autoload.php';
$loop = new Select();Timer::init($loop);$messages = [];$cancelled = Timer::repeat(0.005, static function (): void { throw new RuntimeException('Cancelled timer executed');});Timer::del($cancelled);Timer::delay(0.01, static function (string $message) use (&$messages): void { $messages[] = $message;}, ['hello']);Timer::delay(0.03, static function () use ($loop): void { $loop->stop();});$loop->run();
if ($messages !== ['hello'] || Timer::getAll() !== ['count' => 0]) { throw new RuntimeException('Unexpected timer state');}echo "timer-example-ok\n";php timers.phpПоместите файл рядом с vendor/autoload.php. Ожидается timer-example-ok. Подготовка примеров задаёт закреплённую ревизию исходников.
Жизненный цикл worker и соединения
Заголовок раздела «Жизненный цикл worker и соединения»Регистрируйте таймеры в onServerStart/onConnect по назначению, а не в pre-fork master для request-specific работы. Храните ID у владельца/context и удаляйте при закрытии соединения. Closure с connection удерживает объект в памяти. Не используйте delAll() для очистки одного соединения. Повторяющийся callback должен быть коротким, обрабатывать ошибки и явно координировать пересекающуюся внешнюю работу.
Timer::sleep не всегда асинхронный. С Select он блокирует worker; при suspension-capable backend всё равно нужен поддерживаемый coroutine/Fiber context. Для простого расписания предпочтительны delay/repeat. Сам event loop не делает блокирующие HTTP, SQL и file access асинхронными.