Асинхронні проміси

OxPHP виконує PHP-замикання у фонових потоках, в окремому пулі, відокремленому від пулу HTTP-воркерів. Тривала робота виконується там, а не блокує обробку запитів.

Як це працює

  1. Відправлення — викличте oxphp_async() із замиканням і необовʼязковими аргументами. OxPHP серіалізує use-змінні та аргументи замикання, надсилає їх до асинхронного пулу й одразу повертає ідентифікатор промісу
  2. Виконання — виділений асинхронний потік-воркер десеріалізує дані, виконує замикання та серіалізує результат
  3. Очікування — викличте oxphp_async_await() з ідентифікатором промісу. У режимі воркера з файберами поточний файбер призупиняється, а інші запити продовжують обслуговуватися в тому самому потоці. У традиційному режимі потік воркера блокується, доки результат не буде готовий
  4. Очищення — усі проміси, які не було явно очікувано, автоматично скасовуються й очищаються наприкінці запиту

Налаштування

Змінна Типове значення Опис
ASYNC_WORKERS 0 (вимкнено) Кількість виділених асинхронних потоків-воркерів. Встановіть 0, щоб повністю вимкнути асинхронний пул
ASYNC_QUEUE_CAPACITY 0 (авто) Максимальна кількість асинхронних задач в очікуванні. Якщо 0, типове значення — ASYNC_WORKERS × 64
ASYNC_MAX_FIBERS 256 Обмеження кількості одночасних файберів асинхронних задач на один воркер. Глобальний для процесу ліміт задач у роботі (у черзі + тих, що виконуються) становить ASYNC_MAX_FIBERS × ASYNC_WORKERS; відправлення понад нього одразу (без блокування) відхиляється з OxPHP\Async\AsyncException, тож композиція з розгалуженням не може зайти в дедлок, очікуючи на ресурс, який сама ж утримує
Note

Асинхронний пул типово вимкнено (ASYNC_WORKERS=0). Коли пул вимкнено, усі чотири асинхронні функції існують, але під час виклику кидають OxPHP\Async\AsyncException. Встановіть для ASYNC_WORKERS значення, більше за 0, щоб увімкнути фонове виконання.

Відправлення задач

Передайте замикання й необовʼязкові аргументи до oxphp_async(). Функція одразу повертає ідентифікатор промісу (ціле число):

php
<?php $promise = oxphp_async(function (string $url) { return file_get_contents($url); }, 'https://api.example.com/data'); // The closure is running in the background. // Do other work here... $result = oxphp_async_await($promise); echo $result;

Передавання даних у замикання

Для передавання даних використовуйте use-змінні або аргументи функції. Підтримуються лише скалярні типи та масиви:

php
<?php $apiKey = 'sk-abc123'; $ids = [1, 2, 3]; $promise = oxphp_async(function () use ($apiKey, $ids) { // $apiKey and $ids are available here return count($ids); });

Очікування результатів

Один проміс

php
<?php $result = oxphp_async_await($promise); // Wait indefinitely $result = oxphp_async_await($promise, 5.0); // Wait up to 5 seconds

Тайм-аут 0.0 (типове значення) означає очікування без обмеження часу. Коли тайм-аут спливає, кидається OxPHP\Async\TimeoutException.

Усі проміси

oxphp_async_await_all() очікує на кожен проміс і повертає асоціативний масив, ключами якого є ідентифікатори промісів:

php
<?php $p1 = oxphp_async(fn() => file_get_contents('https://api.example.com/users')); $p2 = oxphp_async(fn() => file_get_contents('https://api.example.com/orders')); $results = oxphp_async_await_all([$p1, $p2], 10.0); $users = $results[$p1]; $orders = $results[$p2];
Note

oxphp_async_await_all() очікує на проміси послідовно, у порядку елементів масиву. Усі замикання виконуються в асинхронному пулі паралельно, але викликальний потік забирає результати по одному.

Перший завершений проміс (race)

oxphp_async_await_race() повертає керування, щойно будь-який проміс завершиться — успішно чи з відхиленням:

php
<?php $p1 = oxphp_async(fn() => fetch_from_primary_db()); $p2 = oxphp_async(fn() => fetch_from_replica_db()); $winner = oxphp_async_await_race([$p1, $p2], 5.0); // $winner = ['id' => int, 'value' => mixed] echo "Promise {$winner['id']} won: {$winner['value']}";

Проміси, що не виграли, залишаються доступними для окремого очікування після повернення oxphp_async_await_race(). Якщо проміс-переможець було відхилено, кидається OxPHP\Async\AsyncException — на решту промісів однаково можна очікувати. Це аналог Promise.race з JavaScript.

Перший успішно виконаний проміс

oxphp_async_await_any() повертає керування, щойно будь-який проміс ВИКОНАЄТЬСЯ успішно. Відхилення накопичуються й стають видимими, лише якщо відхилено всі проміси. Це аналог Promise.any з JavaScript — корисний для сценаріїв із резервуванням («будь-яке дзеркало, що відповість»).

php
<?php $mirror_a = oxphp_async(fn() => fetch('https://mirror-a.example.com/data')); $mirror_b = oxphp_async(fn() => fetch('https://mirror-b.example.com/data')); $mirror_c = oxphp_async(fn() => fetch('https://mirror-c.example.com/data')); try { $winner = oxphp_async_await_any([$mirror_a, $mirror_b, $mirror_c], 5.0); // ['id' => one of the input ids, 'value' => its result] } catch (\OxPHP\Async\AggregateAsyncException $e) { foreach ($e->getErrors() as $i => $err) { // $err keyed by input position 0..N-1 } foreach ($e->getErrorMap() as $promise_id => $err) { // $err keyed by promise id } } catch (\OxPHP\Async\TimeoutException $e) { foreach ($e->getPartialErrors() as $promise_id => $err) { // promises that already rejected before the deadline } $cancelled = $e->getCancelledPromiseIds(); // Promise ids that had not settled at the deadline. The cancel flag // is set on each AND their receivers were dropped — passing any of // these ids to oxphp_async_await*() afterwards throws "unknown or // already-awaited promise id". Treat the list as an audit trail, not // a resumable queue. }

Проміси, що не виграли й на момент перемоги ще очікували завершення, залишаються доступними для окремого очікування. Проміси, відхилені ще до появи переможця, — ні: їхні результати було спожито, коли вони накопичувалися як можливі помилки.

Типи винятків

Клас Хто кидає Примітки
OxPHP\Async\AsyncException oxphp_async_await(), oxphp_async_await_all(), oxphp_async_await_race() Одна помилка з повідомленням і необовʼязковими відомостями про початковий виняток.
OxPHP\Async\TimeoutException Усі чотири функції await-* після спливання тайм-ауту Розширює AsyncException. Для тайм-аутів oxphp_async_await_any() заповнюються getPartialErrors() і getCancelledPromiseIds(); в інших місцях виклику обидва методи повертають [].
OxPHP\Async\AggregateAsyncException oxphp_async_await_any(), коли відхилено всі проміси Розширює AsyncException. Надає getErrors() (позиційні, ключі 0..N-1), getErrorMap() (ключі — ідентифікатори), getPromiseIds().

Обробка помилок

Винятки, кинуті всередині асинхронного замикання, перехоплюються й повторно кидаються під час очікування як OxPHP\Async\AsyncException:

php
<?php $promise = oxphp_async(function () { throw new \RuntimeException('Something failed'); }); try { $result = oxphp_async_await($promise); } catch (\OxPHP\Async\AsyncException $e) { // "Async task failed: [RuntimeException] Something failed" echo $e->getMessage(); }

exit() і die() всередині асинхронного замикання також перехоплюються й перетворюються на OxPHP\Async\AsyncException. Асинхронний воркер залишається працездатним і продовжує обробляти нові задачі.

Ієрархія винятків

text
\Exception └── OxPHP\Async\AsyncException # All async errors ├── OxPHP\Async\TimeoutException # Timeout-specific └── OxPHP\Async\AggregateAsyncException # Every promise rejected (await_any)

Інтеграція з файберами

У режимі воркера oxphp_async_await() взаємодіє з планувальником файберів OxPHP. Замість того щоб блокувати потік воркера, поточний файбер призупиняється на час очікування результату. Планувальник відновлює його, щойно результат готовий, тож інші запити продовжують оброблятися в тому самому потоці.

У традиційному режимі (без файлу воркера) oxphp_async_await() синхронно блокує потік воркера. Поки триває очікування, воркер не може обробляти інші запити.

Для найкращої продуктивності поєднуйте асинхронні проміси з режимом воркера:

worker.php
<?php // worker.php require __DIR__ . '/../vendor/autoload.php'; oxphp_worker(function () { // These two API calls run concurrently on the async pool // while the fiber suspends — the worker thread is free for other requests $p1 = oxphp_async(fn() => file_get_contents('https://api.example.com/users')); $p2 = oxphp_async(fn() => file_get_contents('https://api.example.com/orders')); $results = oxphp_async_await_all([$p1, $p2]); echo json_encode($results); });

Композиція (вкладені асинхронні виклики)

Асинхронна задача може сама викликати oxphp_async() і очікувати на результат. Оскільки кожна задача виконується у файбері планувальника, очікування на вкладений проміс призупиняє файбер задачі й звільняє її воркер для виконання вкладеної задачі — тож задача може розгалужуватися на дочірні, не утримуючи воркер під час очікування.

php
<?php $p = oxphp_async(function (): int { // Dispatched and awaited from inside an async task $inner = oxphp_async(fn () => 21); return oxphp_async_await($inner) * 2; }); $result = oxphp_async_await($p); // 42

oxphp_async_await_all(), oxphp_async_await_race() і oxphp_async_await_any() так само можна викликати з файбера задачі. Кількість одночасних файберів задач (у черзі + тих, що виконуються) обмежена значенням ASYNC_MAX_FIBERS × ASYNC_WORKERS; відправлення, яке перевищило б цей ліміт, одразу відхиляється з OxPHP\Async\AsyncException замість блокування, тож розгалуження не може зайти в дедлок, очікуючи на ресурс, який саме й утримує.

Обмеження

Асинхронні замикання виконуються в окремих потоках. Це накладає обмеження на те, які дані можуть перетинати межу потоку:

Дозволено Не дозволено
null, bool, int, float, string Будь-які інші обʼєкти. OxPHP\Shared\Shareable зарезервовано за типами Shared\*: клас, написаний на PHP, не може ні реалізувати його, ні успадковувати клас, який його реалізує
Масиви скалярних типів Ресурси (дескриптори файлів, зʼєднання з БД, потоки)
Вкладені скалярні масиви Замикання, чий use захоплює обʼєкти, що не є Shareable
Екземпляри Shared\* (Counter, Map, Channel, Atomic, Flag, Mutex, Once, Pool, Registry)

Додаткові обмеження:

  • Лише користувацькі функції — замикання має бути визначене користувачем, а не обгорткою над вбудованою функцією
  • Накладні витрати на серіалізацію — аргументи й значення, що повертаються, серіалізуються під час перетину межі потоку. Великі масиви чи рядки збільшують затримку
  • Немає спільного стану для звичайних значень PHP — кожен асинхронний воркер має власне середовище PHP. Звичайні змінні, масиви й екземпляри класів копіюються (або відхиляються) на межі. Щоб передавати посилання, видимі обом потокам, використовуйте примітиви спільного стану (Shared\Counter, Shared\Map, Shared\Channel, …)
  • Усередині задачі немає завершення скрипту — задача не є запитом, тому register_shutdown_function(), викликана з асинхронного замикання, реєструє зворотний виклик, який ніколи не виконається: немає відповіді, у яку він міг би писати, і немає запиту, завершенням якого він міг би бути, а після завершення задачі його буде відкинуто. Виконуйте цю роботу наприкінці самого замикання
  • Усередині задачі немає дедлайну виконання — max_execution_time не обмежує те, що виконує задача, так само як і set_time_limit(), викликана всередині неї. Обидва виклики й далі завершуються успішно (set_time_limit() повертає true, а ini_set() — попереднє значення, як і будь-де), і значення так само читається через ini_get('max_execution_time'), але ніщо його не відлічує. Рушій тримає один таймер виконання на потік, а потік пулу виконує всі задачі свого життя всередині одного-єдиного запиту, який він відкрив під час запуску. Дедлайн, заведений там, не належав би жодній конкретній задачі й завершив би ту, що саме виконувала PHP-код, коли він сплив, — зокрема й задачу, яка взагалі не просила жодного ліміту. Натомість обмежуйте задачу з боку того, хто на неї чекає: аргументом тайм-ауту oxphp_async_await() та її варіантів, який скасовує задачу, на яку перестали чекати (див. «Тайм-аути скасовують покинуту задачу» в розділі Усунення несправностей). Одне директиву все ж читає: з увімкненим RUNTIME_HOOKS=streams очікування на підключення, яке використовує інший файбер, обмежене меншим із max_execution_time і default_socket_timeout. Задача, що знижує це значення, скорочує таке очікування і для себе, і для задач, які виконуватимуться в цьому потоці після неї, адже ini-значення, задане задачею, не відкочується до завершення запиту, а запит потоку пулу не завершується

Приклад для Docker

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.13.0 ports: - "80:80" environment: - DOCUMENT_ROOT=/var/www/html/public - WORKER_MODE_ENABLED=true - ENTRY_FILE=worker.php - ASYNC_WORKERS=4 - ASYNC_QUEUE_CAPACITY=256

Усунення несправностей

"Async pool is disabled. Set ASYNC_WORKERS > 0 to enable."

Асинхронний пул не налаштовано. Коли ASYNC_WORKERS=0 (типове значення), асинхронні функції зареєстровано, але кожен їхній виклик кидає OxPHP\Async\AsyncException.

Виправлення: встановіть для ASYNC_WORKERS додатне значення:

bash
ASYNC_WORKERS=4
"Failed to dispatch async task (pool full)"

Асинхронний пул працює, але всі слоти черги зайняті або досягнуто глобального для процесу ліміту задач у роботі (ASYNC_MAX_FIBERS × ASYNC_WORKERS, що охоплює задачі в черзі + ті, що виконуються). В обох випадках під час відправлення кидається OxPHP\Async\AsyncException і збільшується oxphp_async_tasks_rejected_total.

Перевірка: переконайтеся, що пул приймає задачі:

bash
curl -s http://localhost:9090/config | jq '.async_workers'

Виправлення: збільште ASYNC_WORKERS або ASYNC_QUEUE_CAPACITY.

"Cannot pass object values in use-vars to async closure"

Обʼєкти не можна серіалізувати для передавання через межі потоків.

Виправлення: перед відправленням витягніть потрібні скалярні дані:

php
<?php // Wrong: passing an object $promise = oxphp_async(function () use ($user) { ... }); // Correct: passing scalar data extracted from the object $userId = $user->getId(); $userName = $user->getName(); $promise = oxphp_async(function () use ($userId, $userName) { ... });
Очікування зависає в традиційному режимі

У традиційному режимі oxphp_async_await() блокує потік воркера. Якщо всі PHP-воркери заблоковані в очікуванні асинхронних результатів, сервер припиняє обробляти запити.

Виправлення: увімкніть режим воркера (WORKER_MODE_ENABLED=true), щоб oxphp_async_await() призупиняла файбер, а не блокувала потік.

Тайм-аути скасовують покинуту задачу

OxPHP\Async\TimeoutException кидається на боці очікування в ту мить, коли спливає тайм-аут. Фонова задача більше не залишається виконуватися без нагляду: задача, припаркована в oxphp_sleep() або призупинена в очікуванні дочірнього промісу, відновлюється й розгортає стек (її блоки finally виконуються), а задача, що навантажує процесор і ніколи не поступається керуванням, переривається на межі опкоду — тож скасування відбувається за принципом best-effort з короткою гарантованою затримкою, а не миттєво. Таке саме скасування застосовується до промісів, які покидає oxphp_async_await_all(), і до промісів, що програли в oxphp_async_await_race() / oxphp_async_await_any(). Задача, яку все одно не вдається вчасно перервати, стає застряглою — скасованою, але дочікуваною наприкінці запиту, що може подовжити RSHUTDOWN на кілька секунд; стежте за oxphp_async_tasks_stranded_total.

Рекомендації

  • Завжди встановлюйте тайм-аути для викликів oxphp_async_await() у продакшені, щоб уникнути нескінченного очікування
  • Використовуйте режим воркера, щоб отримати неблокувальне очікування на основі файберів замість блокування потоку воркера
  • Робіть замикання невеликими — відправляйте сфокусовані одиниці роботи, а не цілі обробники запитів
  • Витягуйте скалярні значення перед відправленням — дістаньте ідентифікатори, рядки й значення конфігурації з обʼєктів, перш ніж передавати їх у замикання
  • Моніторте асинхронний пул — перевіряйте oxphp_async_tasks_rejected_total у метриках Prometheus. Якщо кількість відхилень зростає, збільште ASYNC_WORKERS або ASYNC_QUEUE_CAPACITY

Промпти для вашого агента

Ці промпти передбачають, що до агента підключено MCP-сервер документації OxPHP, — потрібні сторінки він прочитає сам.

prompt
Перенеси повільну роботу з моїх обробників запитів в асинхронний пул OxPHP. Прочитай сторінку `features/async-promises` через MCP-сервер документації OxPHP. Вибери виклики в цьому репозиторії, які варто відправляти в пул, перепиши їх як замикання, що не передають через межу потоку нічого, окрім даних, і назви розмір пулу та місткість черги, з якими їх слід запускати.

Див. також