Je vous parlais de cURL et d’API il y a quelques jours. Aujourd’hui, j’enfonce le clou avec une méthode que j’expérimente pour un futur projet : Comment exécuter une requête asynchrone avec PHP et cURL.

L’origine du problème

Pour rappel, PHP est un langage de programmation séquentiel, impératif ou encore synchrone. C’est à dire qu’à l’exécution d’un script, il va exécuter chaque instruction séquentiellement, l’une après l’autre.
Avant de passer à la suivante, il attendra obligatoirement le résultat de la dernière instruction.

De plus, le protocole HTTP est également synchrone. Cela signifie qu’à chaque requête, le navigateur attend une réponse qu’il va ensuite afficher.

Dans la plupart des cas, ce fonctionnement suffit. L’utilisateur souhaite accéder à une ressource ou la mettre à jour : Pas de problème, la réponse est envoyée dans la foulée. Mais ce n’est pas toujours suffisant.

Quel problème ?

Je l’ai dit : Généralement le fonctionnement séquentiel est suffisant pour nos besoins. Mais il y a des tas de raisons pour vouloir faire de l’asynchrone avec PHP : Écriture de logs, de nombreux UPDATE à la DB, lancement d’une requête cURL à une API, envoie d’un pouet sur Mastodon lorsque j’écris une news, … Ces opérations peuvent être longues et donc donner la sensation aux visiteurs que votre serveur est en rade, ou que votre script est à chier.

Comprenez bien que si vous, en tant que visiteur, ne voulez pas attendre qu’une page s’affiche, il en est de même pour les usagers qui viendront sur votre site.

La requête asynchrone

La solution à cela est en effet la requête asynchrone. Pendant que votre script enregistre les logs ou se connecte à une API, il est inutile de faire attendre l’utilisateur.
On va donc chercher à faire travailler PHP en fond sur ces tâches lourdes, et continuer nos instructions plus légères jusqu’à l’envoi de la réponse au navigateur.

Plusieurs méthodes possibles

Il existe en fait d’autres approches que celle que je vais vous présenter :

  • fsockopen : ou stream_socket_client. Permet d’ouvrir une connexion par socket, afin d’enregistrer par exemple des données avec fwrite.
  • exec : Cette fonction permet de lancer une instruction, comme si vous la tapiez dans un terminal. En ajoutant un  &  à la fin de cette ligne de commande, l’exécution sera réalisée en tâche de fond et PHP n’attendra pas le résultat avant de continuer le script.
  • pg_send_query : Celle-ci supporte une ou plusieurs requêtes asynchrones à une connexion PostgreSQL. A ma connaissance, il n’existe pas d’équivalent pour MySQL.
  • curl_multi_init : Enfin, cette dernière autorise l’exécution de multiples gestionnaires cURL de façon asynchrone.

Pour ma part, voilà comment je procède.

Une requête asynchrone avec PHP et cURL

Le principe est simple si vous connaissez un peu la librairie cURL. Il suffit de lancer une requête cURL à laquelle on définit un timeout, afin que PHP n’attende pas la réponse et passe à la suite :

Oui, c’est tout.
L’option intéressante est CURLOPT_TIMEOUT_MS , qui permet de définir un temps maximum d’exécution de la fonction cURL, en millisecondes.

Attention : Évidemment, cette ressource ne doit être fait que sur le même site, sans quoi le timeout défini ne permet pas de garantir qu’un autre serveur distant ait le temps de répondre.

A la seconde près

J’ai mis 1000, soit une seconde. Cela permet de ne pas trop gêner le visiteur mais c’est aussi le délai minimum pour la résolution du hostname sur les systèmes Unix/Linux, sans quoi une erreur 28 (timeout) sera envoyée à PHP :

En faisant mes recherches, j’ai trouvé quelques articles qui mettaient une valeur de 1ms. Je ne sais pas sur quel type de serveur ils tournent, mais ça ne fonctionne pas, vous pouvez essayer.

Outrepasser cette limite

Mais on peut le faire !
Malheureusement ces articles n’en parlent pas, alors j’ai cherché pour vous 🙂

En effet, avec l’option CURLOPT_NOSIGNAL, on peut ignorer les fonctions qui causent l’envoi d’un signal au processus PHP :

Un cas concret

A présent, voyons comment cela peut nous être utile.

Imaginons un réseau social décentralisé (parait que c’est la mode), où chaque utilisateur peut suivre n’importe qui sur n’importe quelle instance. Le B.A.-BA en somme.
Lorsqu’un utilisateur, suivi par 100 personnes, poste un nouveau contenu, comment géreriez-vous cela ? Comment informer les 100 autres utilisateurs, possiblement sur 100 instances différentes, que notre bon utilisateur a posté quelque chose ? Et surtout, sans gêner l’auteur du post.
Avec cURL et notre pseudo méthode asynchrone, c’est possible.

Il suffit d’appeler une page sur le même domaine/serveur, qui traitera ces 100 requêtes cURL par exemple, en tâche de fond, à la manière d’une tâche CRON.
Pour l’utilisateur, rien ne transparaît, et le message peut se répliquer tranquillement sur chaque instance sans craindre un timeout execution.