Глава 35 · Практика
Паузы, лимиты API и повторные попытки
sleep, usleep и time_nanosleep в реальных интеграциях: когда ждать, когда отложить, когда остановиться.
Пауза нужна текущему процессу: что делает sleep
Поставщик разрешает ограниченное число запросов. Иногда программе нужно подождать перед следующим действием. sleep принимает число секунд и блокирует текущий процесс: следующая строка выполнится после ожидания. Сам по себе sleep не знает лимита поставщика.
Что уже пригодится. циклы, HTTP-клиент
- Отправить допустимый запрос
- Решить, нужна ли пауза
- sleep: процесс ждёт
- Следующая строка программы
sleep(1) — ждать примерно секунду; usleep(100000) — примерно 100 миллисекунд. Это не таймер браузера и не запуск параллельной работы.
<?php
declare(strict_types=1);
echo "Перед паузой\n";
$seconds = 0;
sleep($seconds);
echo "После паузы\n";
Ожидаемый результат
Перед паузой
После паузы
В скачиваемом тесте задано 0 секунд, чтобы он выполнялся мгновенно. Для собственного локального опыта замените 0 на 1: порядок текста сохранится, появится ожидание. Это демонстрация синтаксиса, а не готовый ограничитель частоты.
Следующая ступень. После этого рассматриваем Retry-After, повторные попытки и несколько работников. Долгое ожидание в запросе веб-страницы занимает рабочий процесс; для таких задач нужна очередь.
Почему пауза иногда необходима
429: повторяем чтение только в пределах бюджета
Чтение каталога получило Retry-After: 2. Нужно соблюсти паузу, сохранив предел попыток и общего ожидания.
- Попытка чтения №1
Отправляем разрешённый запрос каталога и учитываем эту попытку в общем бюджете.
- Ответ 429
Провайдер ограничил частоту. Разбираем Retry-After по его формату, а не повторяем немедленно.
- Остались время и попытки?
Пауза 2 секунды должна помещаться в бюджет. Иначе прекращаем цикл или откладываем задание.
- Выдержать паузу
Короткое ожидание занимает worker. Длительную задержку лучше хранить как время будущего запуска.
- Следующая попытка
Возвращаемся к чтению с увеличенным счётчиком. Успех завершает цикл; бюджет не обнуляется.
Это пример повторяемого чтения. Неизвестный исход создания счёта нельзя повторять так же без идемпотентности и проверки статуса.
Поставщик разрешает два запроса в секунду. Импорт из тысячи товаров без ограничения частоты быстро получает 429 и может быть заблокирован. Пауза между запросами помогает одному последовательному CLI-скрипту соблюдать простой темп. sleep принимает секунды, usleep — микросекунды, time_nanosleep — секунды и наносекунды. Более мелкая единица не обещает такую же точность планировщика: фактическая пауза может быть длиннее.
Сон приостанавливает текущий поток выполнения. Пока HTTP-обработчик спит, PHP-FPM-воркер занят; при удерживаемой сессии или транзакции заняты также блокировки. Поэтому длительную рассылку или импорт не запускают прямо из формы. Форма создаёт задание и показывает его состояние, а CLI-воркер выполняет работу. Для длинной задержки лучше записать available_at и освободить воркер, чем держать его спящим час.
Пауза каждого из десяти процессов по 500 мс не ограничит их суммарный поток двумя запросами в секунду. Общая квота требует координации: очередь с нужной параллельностью либо атомарный распределённый лимитер. Лимиты бывают по аккаунту, токену, endpoint и IP; читайте контракт провайдера. Не выдумывайте универсальную частоту для всех API.
Повтор ограничен числом попыток и временем
<?php
declare(strict_types=1);
function fetchWithRetry(callable $send, callable $wait): string {
$spent = 0;
for ($attempt=0; $attempt<3; $attempt++) {
$response = $send();
if ($response['status'] === 200) return $response['body'];
if (!in_array($response['status'],[429,503],true) || $attempt === 2) {
throw new RuntimeException('retry-stopped');
}
$seconds = $response['retryAfter'] ?? (2 ** $attempt);
if (!is_int($seconds) || $seconds < 0 || $seconds > 10 || $spent+$seconds > 10) {
throw new RuntimeException('retry-budget');
}
$wait($seconds);
$spent += $seconds;
}
throw new LogicException('Unreachable');
}
$responses = [['status'=>429,'retryAfter'=>2],['status'=>503],['status'=>200,'body'=>'ready']];
$send = static function () use (&$responses): array { return array_shift($responses); };
$wait = static function (int $seconds): void { echo "wait:$seconds
"; };
echo fetchWithRetry($send,$wait)."
";
Ожидаемый результат
wait:2
wait:2
ready
Тестовый wait только пишет решение, поэтому пример не тормозит запуск. В CLI можно передать обёртку sleep; в очереди — планирование задания. В реальном клиенте общий дедлайн включает время сетевых запросов, а не только сумму пауз.
Retry-After, jitter и неопределённый результат
Retry-After бывает количеством секунд или HTTP-датой. Число нужно проверять как неотрицательное целое с разумным пределом; дату разбирают по HTTP-формату и сравнивают с текущим временем UTC. Если сервер требует ждать дольше допустимого бюджета задачи, её откладывают, а не уменьшают паузу и немедленно снова нагружают сервис. При отсутствии заголовка обычно выбирают экспоненциальную задержку с верхним пределом.
Jitter добавляет случайность, чтобы тысячи клиентов не проснулись одновременно. Он должен сохранять смысл минимальной задержки, указанной сервером. Для воспроизводимого теста генератор случайности и часы передают зависимостями. Нельзя ждать точного числа миллисекунд в тесте: проверяют выбранную политику, верхние границы и количество попыток.
Безопасность повтора зависит от операции. GET обычно можно повторять, если он действительно только читает. POST создания платежа после таймаута имеет неизвестный результат: сервер мог выполнить операцию, а ответ потерялся. Для него нужен ключ идемпотентности и протокол получения состояния. Не повторяйте 400/401/403 как временный сбой, если документация явно не говорит обратного.
<?php
declare(strict_types=1);
function delaySeconds(int $attempt, ?int $retryAfter, int $jitter): int {
if ($attempt < 0 || $attempt > 6 || $jitter < 0 || $jitter > 2) {
throw new InvalidArgumentException('invalid-policy-input');
}
if ($retryAfter !== null) {
if ($retryAfter < 0 || $retryAfter > 60) throw new DomainException('defer-job');
return $retryAfter + $jitter;
}
return min(30, 2 ** $attempt) + $jitter;
}
echo delaySeconds(0,null,1)."
";
echo delaySeconds(2,10,2)."
";
try { delaySeconds(2,3600,0); } catch (DomainException $e) { echo $e->getMessage()."
"; }
Ожидаемый результат
2
12
defer-job
Предел 60 секунд относится к минимальному Retry-After; итог с jitter может достигать 62 секунд. Политика явно откладывает часовой отказ. В рабочем коде jitter получают random_int(0,2), а здесь передают фиксированно.
Тонкости функций ожидания
sleep возвращает 0 при полном ожидании; при прерывании семантика возвращаемого значения зависит от платформы. usleep не возвращает полезного результата, и большие интервалы удобнее выражать sleep или планировщиком. time_nanosleep может вернуть true, false либо массив оставшегося времени при прерывании сигналом. Надёжный воркер должен уметь корректно завершаться по сигналу, а не превращать любое прерывание в бесконечный сон.
Для измерения длительности используйте монотонный hrtime, а для даты следующего задания — календарное время. Системные часы могут корректироваться, поэтому вычисление длительности как time_end-time_start иногда ведёт себя неожиданно. Не используйте busy-wait с пустым циклом: он тратит CPU, хотя никакой полезной работы не происходит.
Медленное место здесь намеренное: ожидание защищает внешний сервис. Оптимизация не означает убрать sleep и получить больше 429. Полезные показатели — число успешных операций, задержка очереди, доля повторов и соблюдение квоты. Если одному импорту мешают другие, нужна координация общего лимита, а не случайное увеличение паузы в каждой копии скрипта.
Общий лимитер и честное состояние задания
Token bucket позволяет небольшие всплески в пределах ёмкости, а leaky bucket задаёт более ровный темп. Выбор зависит от правил провайдера: средняя скорость не всегда заменяет жёсткий лимит каждого окна. При нескольких воркерах изменение счётчика и выдача разрешения должны быть атомарными. Обычные get/set в Redis без согласованного алгоритма оставляют гонку так же, как SELECT перед INSERT.
Состояние задания хранит номер попытки, последнее безопасное описание ошибки, время следующего запуска и срок окончательного отказа. Если сервис попросил ждать час, интерфейс должен показать отложенную обработку, а не крутить бесконечный спиннер. Отмена задания прекращает будущие попытки, но не способна отменить уже принятый внешний запрос; это отдельная операция по контракту API.
Разделяйте лимит скорости и ограничение параллельности. Десять медленных запросов могут укладываться в секундную квоту, но занять все соединения. Маленькая очередь и ограниченный пул защищают память и воркеры. Метрики успешных операций, возраста очереди и доли 429 помогают понять, где узкое место: в квоте партнёра или в собственной обработке.
Практика: проверьте решение, а не только успешный путь
Нужно отправить 20 000 уведомлений с лимитом 5/с. Почему sleep(1) внутри веб-запроса после каждого пятого сообщения плохой проект?
Разбор решения
Задание живёт дольше HTTP-таймаутов, занимает воркер и теряет прогресс при обрыве. Сохраните получателей и состояние в очереди, ограничьте общий темп, фиксируйте успешные отправки и используйте идемпотентность. Веб-страница показывает прогресс и позволяет отменить дальнейшую работу.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
random_intВыбрать непредсказуемое целое без самодельного масштабирования и смещения вероятностей.
random_int(int $min, int $max): intВыбрать непредсказуемое целое без самодельного масштабирования и смещения вероятностей.
| Часть вызова | Договор |
|---|---|
| min / max | Нижняя/верхняя границы PHP int, обе включены; min≤max. |
| Возврат | Равномерно выбранный int из [min,max]. Не обещание уникальности при повторных вызовах. |
| Ошибки | ValueError при max<min. Отказ CSPRNG — Random\RandomException с PHP 8.2, до этого Exception; неверный тип в strict_types — TypeError. |
- Границы 5 и 15 включены
- PHP выбирает один из 11 вариантов
- Проверяем диапазон; при [8,8] получается 8
Два аргумента задают включённые границы: random_int(5, 15) может вернуть и 5, и 15. Знак $ обозначает переменную с результатом; сравнения проверяют, что её значение подходит договору. Здесь нет seed: системную криптографическую случайность не настраивают временем компьютера.
<?php
declare(strict_types=1);
$quantity = random_int(5, 15);
echo $quantity >= 5 && $quantity <= 15 ? "От 5 до 15 включительно\n" : "За пределами\n";
echo random_int(8, 8), "\n";
Ожидаемый результат
От 5 до 15 включительно
8
Первый результат меняется, поэтому expected фиксирует его полезный договор. Равные границы допустимы и дают единственное число, но такой диапазон не содержит непредсказуемости.
<?php
declare(strict_types=1);
function verificationCode(): string
{
return sprintf('%06d', random_int(0, 999999));
}
$code = verificationCode();
echo strlen($code), '/', ctype_digit($code) ? 'digits' : 'other', "\n";
Ожидаемый результат
6/digits
sprintf дополняет число до шести цифр, сохраняя варианты вроде 000007. Миллион вариантов — ограниченный поиск: реальная проверка требует срока и ограничения попыток. Это создание строки, а не готовая система входа; код никуда не отправляется.
<?php
declare(strict_types=1);
try { random_int(15, 5); }
catch (ValueError) { echo "Нижняя граница больше верхней\n"; }
Ожидаемый результат
Нижняя граница больше верхней
Не вычисляйте random_bytes()%range вручную: остаток обычно даёт смещение вероятностей. Функция сама выбирает равномерное число в заданном замкнутом диапазоне.
Поддерживается диапазон PHP int, включая отрицательные значения; он зависит от разрядности сборки. Для длительных секретов обычно удобнее random_bytes с достаточным пространством вариантов. Разные вызовы могут дать одинаковое число: непредсказуемость и отсутствие повторов — разные требования. Системные источники и старые версии описаны в общей таблице.
Продолжить: random_bytes, Random\Randomizer::getInt.
Как выбрать случайность и прочитать первый вызов.
Обе границы включены. Возвращает int; неверный диапазон даёт ValueError, сбой источника случайности — исключение.
<?php
declare(strict_types=1);
$jitter=random_int(0,250);
echo $jitter>=0 && $jitter<=250 ? "within milliseconds budget\n" : "invalid\n";
Ожидаемый результат
within milliseconds budget
Тест проверяет контракт диапазона, не конкретное случайное значение.
<?php
declare(strict_types=1);
try { random_int(10,1); } catch (ValueError) { echo "invalid range\n"; }
Ожидаемый результат
invalid range
Ошибку конфигурации обнаруживают до запуска партии фоновых заданий.
Рядом по задаче. random_bytes создаёт токены; mt_rand допустим для несекретных задач, но не паролей и CSRF.
sleepЗаблокировать текущий процесс на целое число секунд.
sleep(int $seconds): intСтоимость: ожидание. Текущий процесс занят паузой. Длительные повторы выносите из HTTP-запроса в управляемую фоновую задачу.
Заблокировать текущий процесс на целое число секунд.
Скобки после имени запускают вызов; значения внутри — аргументы. Знак = сохраняет возвращённый результат в переменную со знаком $. Функция может вернуть число, строку, массив, true/false либо завершить текущий сценарий. Сначала смотрите минимальный вызов и ожидаемый результат, затем переходите к условиям, функциям и обработке ошибок. Как устроен вызов, параметр и return.
Что произойдёт при вызове
- Рабочая операция завершилась
- Определена ограниченная пауза
- sleep блокирует этот процесс
- После возврата выполняется следующая строка
Первый запуск и развитие задачи
Пауза не является глобальным rate limiter: несколько воркеров спят независимо. Не держите во время долгого сна HTTP-запрос, сессию или транзакцию; отложите задачу в очередь.
Полный договор вызова
| Часть вызова | Полный договор |
|---|---|
| seconds | Целое число ≥ 0. Дробь вроде 0.25 не задаёт четверть секунды. |
| Возврат 0 | Запрошенное ожидание завершилось полностью. |
| Прерывание | На Unix возвращаются оставшиеся секунды; на Windows при прерывании возвращается 192. |
| Отрицательное значение | ValueError с PHP 8; раньше предупреждение и false. |
Ошибки и границы. Сигнал может прервать ожидание. Клиентский disconnect обнаруживается только на границе вывода; sleep сам по себе не проверяет соединение.
Версии и платформа. Длительность может быть больше запрошенной из-за планировщика. Поведение прерывания различается по ОС.
Рядом: usleep, time_nanosleep, time_sleep_until.
Возвращает 0 при полном ожидании. При прерывании результат зависит от платформы; отрицательное значение вызывает ValueError. Процесс занят всё время ожидания.
<?php
declare(strict_types=1);
function importBatches(array $products, callable $wait): void
{
$batches = array_chunk($products, 2);
foreach ($batches as $index => $batch) {
// Здесь рабочий импорт отправил бы пакет и проверил результат.
echo 'Обработан пакет ', $index + 1, ': ', count($batch), " товара\n";
if ($index < count($batches) - 1) {
$wait(2);
}
}
}
$fakeWait = static function (int $seconds): void {
echo 'Пауза ', $seconds, " с\n";
};
importBatches(['TEA', 'COFFEE', 'HONEY', 'MILK'], $fakeWait);
// Эта альтернатива показана для рабочего CLI; в примере не вызывается.
$realWait = static function (int $seconds): void {
if (PHP_SAPI !== 'cli' || $seconds < 0 || $seconds > 10) {
throw new InvalidArgumentException('Нужна ограниченная CLI-пауза');
}
if (sleep($seconds) !== 0) {
throw new RuntimeException('Ожидание прервано');
}
};
Ожидаемый результат
Обработан пакет 1: 2 товара
Пауза 2 с
Обработан пакет 2: 2 товара
Поставщик разрешает небольшие пакеты с промежутком. Подставной wait показывает точное место ожидания без задержки теста; после последнего пакета ждать незачем. В рабочем CLI передают $realWait вместо $fakeWait. Здесь нет реальной отправки и глобального rate limiter: учитывайте Retry-After, фактические квоты и других воркеров. Длинное ожидание переносите в очередь, не держите сессию или транзакцию.
<?php
declare(strict_types=1);
try { sleep(-1); } catch (ValueError) { echo "invalid delay\n"; }
Ожидаемый результат
invalid delay
Пример не ждёт. Такую конфигурацию лучше отвергнуть до запуска импорта.
Рядом по задаче. usleep для долей секунды, time_nanosleep для явного остатка при прерывании, ограниченный retry.
usleepЗаблокировать текущий процесс на заданное число микросекунд.
usleep(int $microseconds): voidСтоимость: ожидание. Текущий процесс занят паузой. Длительные повторы выносите из HTTP-запроса в управляемую фоновую задачу.
Заблокировать текущий процесс на заданное число микросекунд.
Скобки после имени запускают вызов; значения внутри — аргументы. Знак = сохраняет возвращённый результат в переменную со знаком $. Функция может вернуть число, строку, массив, true/false либо завершить текущий сценарий. Сначала смотрите минимальный вызов и ожидаемый результат, затем переходите к условиям, функциям и обработке ошибок. Как устроен вызов, параметр и return.
Что произойдёт при вызове
- Рассчитать целое число микросекунд
- Проверить допустимый предел
- usleep блокирует текущий процесс
- Выполнение продолжается после паузы
Первый запуск и развитие задачи
Для целых секунд читаемее sleep. В unit-тесте политику ожидания передавайте callback/объект и записывайте запрос без настоящего сна.
Полный договор вызова
| Часть вызова | Полный договор |
|---|---|
| microseconds | Микросекунда — миллионная доля секунды: 1 000 мкс = 1 мс. На некоторых ОС значения больше 1 000 000 не поддерживаются. |
| Возврат | Полезного значения нет (void). |
| Фактическое время | Может оказаться больше из-за нагрузки, вызова функции и гранулярности таймера. |
Ошибки и границы. Не передавайте float: сначала сформулируйте единицу и округление. Пауза не координирует несколько процессов.
Версии и платформа. Граница >1 секунды зависит от ОС; Manual рекомендует sleep для такого интервала.
Рядом: sleep, time_nanosleep, hrtime.
Принимает неотрицательное число микросекунд и не возвращает полезное значение. Реальное время может быть больше из-за планировщика; отрицательное значение недопустимо.
<?php
declare(strict_types=1);
function waitJitterMilliseconds(int $milliseconds): void {
if ($milliseconds<0 || $milliseconds>500) throw new InvalidArgumentException('jitter');
usleep($milliseconds*1000);
}
Определение без ожидания. В рабочем CLI значение можно получить random_int(0,250); это дополнение к backoff, а не общий лимитер.
<?php
declare(strict_types=1);
try { usleep(-1); } catch (ValueError) { echo "invalid microseconds\n"; }
Ожидаемый результат
invalid microseconds
Никакой реальной паузы в тесте нет.
Рядом по задаче. sleep проще для секунд; fake wait ускоряет тесты политики.
time_nanosleepЗаблокировать текущий процесс на заданные секунды и наносекунды.
time_nanosleep(int $seconds, int $nanoseconds): array|boolСтоимость: ожидание. Текущий процесс занят паузой. Длительные повторы выносите из HTTP-запроса в управляемую фоновую задачу.
Заблокировать текущий процесс на заданные секунды и наносекунды.
Скобки после имени запускают вызов; значения внутри — аргументы. Знак = сохраняет возвращённый результат в переменную со знаком $. Функция может вернуть число, строку, массив, true/false либо завершить текущий сценарий. Сначала смотрите минимальный вызов и ожидаемый результат, затем переходите к условиям, функциям и обработке ошибок. Как устроен вызов, параметр и return.
Что произойдёт при вызове
- Миллисекунды разделить на секунды и остаток
- Проверить обе границы
- time_nanosleep ждёт
- true либо остаток после сигнала
Первый запуск и развитие задачи
На Windows фактический сон может быть длиннее из-за аппаратной/системной гранулярности. Не путайте requested delay с измеренной длительностью; для проверки измеряйте hrtime.
Полный договор вызова
| Часть вызова | Полный договор |
|---|---|
| seconds | Неотрицательное целое число. |
| nanoseconds | Неотрицательное целое строго меньше 1 000 000 000. |
| true / false | true — пауза завершена; false — отказ вызова. |
| Массив остатка | При прерывании: seconds и nanoseconds, оставшиеся до конца. Политика повтора принадлежит приложению. |
Ошибки и границы. Неверные диапазоны должны быть отвергнуты до ожидания. Возвращённый массив нельзя считать успешным полным ожиданием.
Версии и платформа. Сигналы и доступность высокоточного ожидания зависят от платформы.
При полном ожидании возвращает true; при неуспехе false, а при прерывании сигналом может вернуть массив seconds/nanoseconds оставшегося времени. Наносекунды должны быть от 0 до 999999999.
<?php
declare(strict_types=1);
function nanosleepHalfSecond(): void {
$result=time_nanosleep(0,500_000_000);
if ($result===false) throw new RuntimeException('Wait failed');
if (is_array($result)) throw new RuntimeException('Wait interrupted; let worker decide');
}
Функция не вызывается; реальный воркер решает, продолжать ли остаток после сигнала.
<?php
declare(strict_types=1);
function waitMilliseconds(int $milliseconds): array|bool {
if ($milliseconds<0 || $milliseconds>2000) throw new InvalidArgumentException('delay');
return time_nanosleep(intdiv($milliseconds,1000),($milliseconds%1000)*1_000_000);
}
Определение без сна. Отдельные секунды предотвращают неверное значение nanoseconds ≥ 1 000 000 000.
Рядом по задаче. sleep/usleep проще для обычных пауз; очередь лучше для длительного отложения.