Promesses asynchrones

OxPHP exécute des closures PHP dans des threads d'arrière-plan, sur un pool dédié distinct du pool de workers HTTP. Le travail de longue durée y est déporté au lieu de bloquer le traitement des requêtes.

Fonctionnement

  1. Répartition — appelez oxphp_async() avec une closure et des arguments optionnels. OxPHP sérialise les variables use de la closure ainsi que ses arguments, les envoie au pool asynchrone et retourne immédiatement un ID de promesse
  2. Exécution — un thread de worker asynchrone dédié désérialise les données, exécute la closure et sérialise le résultat
  3. Attente — appelez oxphp_async_await() avec l'ID de promesse. En mode worker avec Fibers, la Fiber courante est suspendue et les autres requêtes continuent sur le même thread. En mode traditionnel, le thread de worker se bloque jusqu'à ce que le résultat soit prêt
  4. Nettoyage — toute promesse qui n'est pas explicitement attendue est automatiquement annulée et nettoyée à la fin de la requête

Configuration

Variable Valeur par défaut Description
ASYNC_WORKERS 0 (désactivé) Nombre de threads de workers asynchrones dédiés. Mettez 0 pour désactiver entièrement le pool asynchrone
ASYNC_QUEUE_CAPACITY 0 (auto) Nombre maximal de tâches asynchrones en attente. Lorsqu'elle vaut 0, la valeur par défaut est ASYNC_WORKERS × 64
ASYNC_MAX_FIBERS 256 Plafond, par worker, du nombre de Fibers de tâches asynchrones concurrentes. La limite globale du processus (tâches en file + en cours d'exécution) est ASYNC_MAX_FIBERS × ASYNC_WORKERS ; une répartition qui la dépasse est rejetée immédiatement (sans blocage) avec OxPHP\Async\AsyncException, de sorte qu'une composition en éventail ne peut pas se retrouver en interblocage à attendre une capacité qu'elle détient elle-même
Note

Le pool asynchrone est désactivé par défaut (ASYNC_WORKERS=0). Lorsque le pool est désactivé, les quatre fonctions asynchrones existent mais lèvent OxPHP\Async\AsyncException lors de leur appel. Donnez à ASYNC_WORKERS une valeur supérieure à 0 pour activer l'exécution en arrière-plan.

Répartition des tâches

Passez une closure et des arguments optionnels à oxphp_async(). Elle retourne immédiatement un ID de promesse (entier) :

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;

Passer des données aux closures

Utilisez des variables use ou des arguments de fonction pour transmettre des données. Seuls les types scalaires et les tableaux sont pris en charge :

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); });

Attendre les résultats

Promesse unique

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

Un délai d'expiration de 0.0 (la valeur par défaut) attend indéfiniment. En cas d'expiration, OxPHP\Async\TimeoutException est levée.

Toutes les promesses

oxphp_async_await_all() attend chaque promesse et retourne un tableau associatif indexé par ID de promesse :

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() attend les promesses de façon séquentielle, dans l'ordre du tableau. Toutes les closures s'exécutent en concurrence sur le pool asynchrone, mais le thread appelant collecte les résultats un par un.

Première promesse résolue (race)

oxphp_async_await_race() retourne dès qu'une promesse est résolue, qu'elle ait été tenue ou rejetée :

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']}";

Les promesses non gagnantes restent attendables individuellement après le retour de oxphp_async_await_race(). Si la promesse gagnante a été rejetée, OxPHP\Async\AsyncException est levée — les perdantes restent attendables. C'est l'équivalent du Promise.race de JavaScript.

Première promesse tenue

oxphp_async_await_any() retourne dès qu'une promesse est TENUE. Les rejets sont accumulés et ne deviennent observables que si toutes les promesses sont rejetées. C'est l'équivalent du Promise.any de JavaScript — utile pour les schémas de repli / redondance (« n'importe quel miroir qui répond »).

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. }

Les promesses non gagnantes encore en attente au moment de la victoire restent attendables individuellement. Les promesses déjà rejetées avant la gagnante ne le sont pas — leurs résultats ont été consommés lorsqu'ils ont été accumulés comme erreurs candidates.

Types d'exception

Classe Levée par Remarques
OxPHP\Async\AsyncException oxphp_async_await(), oxphp_async_await_all(), oxphp_async_await_race() Erreur unique, avec un message et, en option, les détails de l'exception d'origine.
OxPHP\Async\TimeoutException Les quatre await-* à l'échéance Étend AsyncException. Pour les expirations de oxphp_async_await_any(), getPartialErrors() et getCancelledPromiseIds() sont renseignées ; pour les autres points d'appel, les deux retournent [].
OxPHP\Async\AggregateAsyncException oxphp_async_await_any() lorsque toutes les promesses sont rejetées Étend AsyncException. Fournit getErrors() (positionnel, indexé de 0 à N-1), getErrorMap() (indexé par id), getPromiseIds().

Gestion des erreurs

Les exceptions levées à l'intérieur d'une closure asynchrone sont capturées puis relevées au moment de l'attente sous la forme d'une 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() et die() à l'intérieur d'une closure asynchrone sont également interceptés et convertis en OxPHP\Async\AsyncException. Le worker asynchrone survit et continue de traiter de nouvelles tâches.

Hiérarchie des exceptions

text
\Exception └── OxPHP\Async\AsyncException # All async errors ├── OxPHP\Async\TimeoutException # Timeout-specific └── OxPHP\Async\AggregateAsyncException # Multiple failures (await_all / await_any)

Intégration avec les Fibers

En mode worker, oxphp_async_await() coopère avec l'ordonnanceur de Fibers d'OxPHP. Au lieu de bloquer le thread de worker, la Fiber courante est suspendue pendant qu'elle attend le résultat. L'ordonnanceur la reprend une fois le résultat prêt, si bien que les autres requêtes continuent d'avancer sur le même thread.

En mode traditionnel (sans fichier worker), oxphp_async_await() bloque le thread de worker de manière synchrone. Le worker ne peut pas traiter d'autres requêtes pendant qu'il attend.

Pour de meilleures performances, combinez les promesses asynchrones avec le mode worker :

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); });

Composition (async imbriqué)

Une tâche asynchrone peut elle-même appeler oxphp_async() et en attendre le résultat. Comme chaque tâche s'exécute à l'intérieur d'une Fiber de l'ordonnanceur, attendre une promesse imbriquée suspend la Fiber de la tâche et libère son worker pour exécuter la tâche imbriquée — une tâche peut donc se déployer en éventail vers des enfants sans monopoliser un worker pendant qu'elle attend.

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() et oxphp_async_await_any() peuvent également être appelées depuis l'intérieur d'une Fiber de tâche. Le nombre de Fibers de tâches concurrentes (en file + en cours d'exécution) est borné par ASYNC_MAX_FIBERS × ASYNC_WORKERS ; une répartition qui dépasserait le plafond est rejetée immédiatement avec OxPHP\Async\AsyncException au lieu de bloquer, de sorte qu'un déploiement en éventail ne peut pas se retrouver en interblocage à attendre une capacité qu'il détient lui-même.

Limitations

Les closures asynchrones s'exécutent sur des threads distincts. Cela impose des restrictions sur les données qui peuvent franchir la frontière entre threads :

Autorisé Non autorisé
null, bool, int, float, string Objets ordinaires (toute classe n'implémentant pas OxPHP\Shared\Shareable)
Tableaux de types scalaires Ressources (descripteurs de fichiers, connexions BD, flux)
Tableaux scalaires imbriqués Closures dont le use capture des objets non-Shareable
Instances de Shared\* (Counter, Map, Channel, Atomic, Flag, Mutex, Once, Pool, Registry) et autres classes implémentant OxPHP\Shared\Shareable

Contraintes supplémentaires :

  • Fonctions utilisateur uniquement — la closure doit être définie par l'utilisateur, et non être une enveloppe autour d'une fonction native
  • Surcoût de sérialisation — les arguments et les valeurs de retour sont sérialisés à travers la frontière entre threads. Les grands tableaux ou chaînes de caractères ajoutent de la latence
  • Aucun état partagé pour les valeurs PHP ordinaires — chaque worker asynchrone possède son propre environnement PHP. Les variables, tableaux et instances de classe ordinaires sont copiés (ou rejetés) à travers la frontière. Utilisez les primitives d'état partagé (Shared\Counter, Shared\Map, Shared\Channel, …) pour transmettre des références visibles par les deux threads

Exemple Docker

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.10.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

Dépannage

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

Le pool asynchrone n'est pas configuré. Lorsque ASYNC_WORKERS=0 (la valeur par défaut), les fonctions asynchrones sont enregistrées mais lèvent OxPHP\Async\AsyncException à chaque appel.

Correction : donnez à ASYNC_WORKERS une valeur positive :

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

Le pool asynchrone est en cours d'exécution mais tous les emplacements de la file sont occupés, ou le plafond global du processus (ASYNC_MAX_FIBERS × ASYNC_WORKERS, couvrant les tâches en file + en cours d'exécution) a été atteint. Les deux cas lèvent OxPHP\Async\AsyncException à la répartition et incrémentent oxphp_async_tasks_rejected_total.

Vérification : assurez-vous que le pool accepte les tâches :

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

Correction : augmentez ASYNC_WORKERS ou ASYNC_QUEUE_CAPACITY.

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

Les objets ne peuvent pas être sérialisés à travers les frontières entre threads.

Correction : extrayez les données scalaires dont vous avez besoin avant la répartition :

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) { ... });
L'attente reste bloquée en mode traditionnel

En mode traditionnel, oxphp_async_await() bloque le thread de worker. Si tous les workers PHP sont bloqués à attendre des résultats asynchrones, le serveur cesse de traiter les requêtes.

Correction : activez le mode worker (WORKER_MODE_ENABLED=true) afin que oxphp_async_await() suspende la Fiber au lieu de bloquer le thread.

Les expirations annulent la tâche abandonnée

OxPHP\Async\TimeoutException est levée du côté de l'attente à l'instant même où l'échéance est dépassée. La tâche d'arrière-plan n'est plus laissée à s'exécuter sans surveillance : une tâche mise en pause dans oxphp_sleep() ou suspendue à attendre une promesse enfant est reprise et se déroule (ses blocs finally s'exécutent), et une tâche gourmande en CPU qui ne rend jamais la main est interrompue à une frontière d'opcode — l'annulation est donc « au mieux », avec une faible borne de latence, et non instantanée. La même annulation s'applique aux promesses qu'oxphp_async_await_all() abandonne ainsi qu'aux perdantes de oxphp_async_await_race() / oxphp_async_await_any(). Une tâche qui ne peut toujours pas être interrompue à temps est bloquée — annulée mais purgée à la fin de la requête, ce qui peut allonger RSHUTDOWN de quelques secondes ; surveillez oxphp_async_tasks_stranded_total.

Bonnes pratiques

  • Définissez toujours des délais d'expiration sur les appels à oxphp_async_await() en production pour éviter les attentes indéfinies
  • Utilisez le mode worker pour bénéficier d'une attente non bloquante fondée sur les Fibers plutôt que de bloquer le thread de worker
  • Gardez des closures compactes — répartissez des unités de travail ciblées, pas des gestionnaires de requêtes entiers
  • Extrayez les scalaires avant la répartition — sortez les IDs, chaînes et valeurs de configuration des objets avant de les passer à la closure
  • Surveillez le pool asynchrone — vérifiez oxphp_async_tasks_rejected_total dans les métriques Prometheus. Si les rejets augmentent, augmentez ASYNC_WORKERS ou ASYNC_QUEUE_CAPACITY

Voir aussi