Référence de configuration

OxPHP se configure au moyen de variables d'environnement, plus un fichier facultatif : .oxphpdeny, les règles de refus de chemins lues depuis le sommet de DOCUMENT_ROOT. Chaque paramètre possède une valeur par défaut, si bien qu'un déploiement sans configuration fonctionne d'emblée.

Valeurs booléennes

Les variables marquées comme booléennes acceptent un ensemble canonique fixe, insensible à la casse et débarrassé des espaces superflus :

  • vraies : on, true, 1, yes
  • fausses : off, false, 0, no

Toute valeur non vide en dehors de cet ensemble — des fautes de frappe comme ture — échoue immédiatement au démarrage avec une erreur nommant la variable. On repère ainsi une mauvaise configuration avant l'arrivée du trafic, au lieu de basculer silencieusement un indicateur dans le mauvais sens.

Une variable non définie ou une affectation vide (FOO=) retombe sur la valeur par défaut documentée. Une valeur vide est traitée comme non définie à dessein : une substitution Docker Compose / Kubernetes du type FOO=${FOO} produit FOO= lorsque la variable de l'hôte est absente, et cela ne doit pas empêcher le serveur de démarrer.

Serveur

Variable Valeur par défaut Description
LISTEN_ADDR 0.0.0.0:80 Adresse et port du serveur HTTP principal
DOCUMENT_ROOT /var/www/html/public Répertoire racine pour servir les fichiers et les scripts PHP
ENTRY_FILE (non défini) Script d'entrée canonique unique. Non défini = mappage direct des fichiers. *.php = contrôleur frontal. Non-.php = repli statique (SPA). Avec WORKER_MODE_ENABLED=true = script d'amorçage du worker. Résolu par rapport à DOCUMENT_ROOT (chemins relatifs et .. autorisés ; chemins absolus utilisés tels quels). Voir Routage
WORKER_MODE_ENABLED false Active le mode worker persistant. Nécessite que ENTRY_FILE pointe vers un script .php. Booléen — voir Valeurs booléennes
MAX_CONNECTIONS 10000 Nombre maximal de connexions TCP simultanées. Sert aussi de plafond au QUEUE_MAX_WAITING par défaut (sa moitié), si bien qu'une valeur malformée est une erreur au démarrage plutôt qu'un repli silencieux. L'abaisser ne déplace pas la valeur par défaut de QUEUE_CAPACITY, dimensionnée d'après le seul nombre de workers — gardez-le au-dessus de PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING, voir Garder de la marge pour la boucle d'acceptation
TOKIO_WORKERS CPU / 2 (min. 1) Threads d'E/S asynchrones. 1 = mono-thread, N > 1 = nombre de threads fixe, non défini = automatique (CPU / 2, min. 1). Voir Dimensionner le runtime asynchrone

Dimensionner le runtime asynchrone

TOKIO_WORKERS vaut par défaut la moitié des cœurs disponibles, et la moitié qu'il laisse n'est pas inoccupée : elle revient au pool de workers PHP. Les deux ensembles de threads se disputent les mêmes cœurs, si bien qu'on ne peut pas élargir le runtime sans prendre des cycles à l'application.

Le porter au nombre total de cœurs est une perte partout où le pool est déjà dimensionné près de ce nombre. Mesuré sur une machine à 4 cœurs avec un pool de 4 workers servant des réponses compressées, passer de 2 threads de runtime à 4 a coûté 13 % du débit sur un corps JSON de 1,6 Ko et 26 % sur un corps de 5 Ko, tout en consommant davantage de CPU, parce que les threads étaient en sursouscription.

Augmentez-le là où l'équilibre est différent : un pool petit par rapport à la machine, ou un travail limité par les E/S plutôt que par PHP (upstreams proxifiés, fichiers statiques, réponses en streaming), où c'est le runtime qui travaille et le pool qui attend surtout. Chaque fois que vous le modifiez, surveillez le pool autant que le débit : oxphp_busy_workers rapporté à oxphp_workers_current vous dit de quel côté les cœurs sont partis. Un débit qui monte pendant que le pool s'occupe davantage est un vrai gain ; un débit qui monte pendant que le pool se vide signifie en général que le travail s'est déplacé, pas qu'il s'en est fait davantage.

Tip

TOKIO_WORKERS et PHP_WORKERS valent tous deux par défaut la moitié des cœurs disponibles, si bien que leur somme tombe sur le nombre de cœurs. Modifiez l'un et la somme bouge : c'est pourquoi l'un comme l'autre mérite d'être mesuré plutôt que deviné.

Workers PHP

Variable Valeur par défaut Description
EXECUTOR sapi Backend d'exécution PHP. sapi pour l'exécution PHP, stub pour le benchmarking sans PHP
PHP_WORKERS CPU / 2 (min. 1) Taille du pool de workers. N = pool fixe, MIN:MAX = mise à l'échelle dynamique, 0 = automatique
PHP_WORKERS_IDLE_SECONDS 30 Nombre de secondes pendant lesquelles un worker dynamique reste inactif avant d'être retiré (mode dynamique uniquement). Un worker est retiré à un moment où il n'a rien en cours, si bien que rien de ce qu'il sert n'est interrompu — et un worker qui détient une requête qui ne se termine jamais, comme un flux ouvert, n'atteint jamais un tel moment et n'est pas retiré du tout
QUEUE_CAPACITY Workers initiaux × 128 Nombre maximal de requêtes en attente dans la file PHP. Les requêtes arrivant sur une file pleine attendent une place (voir QUEUE_WAIT_TIMEOUT_MS). Pour les pools dynamiques (MIN:MAX), le nombre de workers initiaux = le minimum. 0 = automatique
QUEUE_WAIT_TIMEOUT_MS 1000 Durée maximale pendant laquelle une requête peut attendre un worker PHP avant d'être rejetée avec un 529. C'est un plafond plutôt qu'une valeur fixe. Une seule échéance, estampillée à l'arrivée, couvrant les deux attentes qu'une requête peut affronter : pour une place dans la file, puis dans la file pour un worker. En surcharge, le serveur raccourcit de lui-même le budget sous ce plafond et le rallonge une fois la file résorbée ; oxphp_admission_wait_budget_us publie la valeur en vigueur, oxphp_admission_wait_budget_ceiling_us celle qui a été configurée (voir Le budget d'attente adaptatif). L'échéance est appliquée par les deux bouts : la requête reçoit un 529 quand elle est dépassée, et un worker qui atteint une requête au-delà refuse de l'exécuter, si bien qu'une requête en file que personne ne prend en charge n'attend pas indéfiniment. Elle ne borne pas la durée d'exécution du gestionnaire ensuite. Au plus QUEUE_MAX_WAITING requêtes attendent à la fois ; au-delà, les requêtes sont rejetées immédiatement. 0 = rejeter immédiatement dès que la file est pleine ; cela supprime l'attente d'une place dans la file, mais pas l'attente à l'intérieur de la file. Abaissez-le quand l'application rappelle ce même serveur en HTTP (l'appel intérieur a besoin d'un worker que l'appel extérieur détient, l'attente est donc dépensée pour rien), ou quand un répartiteur de charge en amont a un délai plus court que lui. Un client qui ferme la connexion en pleine attente rend sa place immédiatement, en HTTP/1.1 comme en HTTP/2 ; celui qui ferme alors qu'il est déjà dans la file garde sa place jusqu'à ce qu'un worker l'atteigne ou que le budget s'épuise, et reçoit alors un 499 au lieu d'être compté comme un refus pour surcharge. Ce que le serveur ne peut pas voir, c'est un client qui cesse d'attendre sans fermer. Un tel client garde sa place jusqu'à son admission ou l'épuisement du budget, et si un worker se libère d'abord, son script s'exécute pour personne : garder le budget en dessous du délai de tout ce qui se trouve devant reste donc une bonne idée
QUEUE_MAX_WAITING Workers initiaux × 128, plafonné à MAX_CONNECTIONS / 2 Nombre maximal de requêtes stationnées en attente d'une place dans la file au même moment. Au-delà, les requêtes sont rejetées immédiatement au lieu d'attendre. Chaque requête en attente détient une connexion et un corps de requête entièrement mis en tampon aussi longtemps qu'elle attend, c'est donc une borne sur les ressources détenues, pas sur les attentes qui aboutiront. Le plafond MAX_CONNECTIONS / 2 de la valeur par défaut ne borne que cette partie de l'arriéré ; les requêtes en file et en cours d'exécution détiennent une connexion de la même manière, si bien que la marge que le serveur garde réellement pour accepter et refuser découle de PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING face à MAX_CONNECTIONS — voir plus bas. Dimensionnez-le d'après la latence de votre propre gestionnaire — plus bas également. 0 = automatique, jamais en dessous de 1
QUEUE_MAX_WAITING_BYTES 67108864 (64 Mio) Nombre maximal d'octets de corps de requête que les requêtes stationnées peuvent détenir à elles toutes. Une requête dont le corps ferait dépasser ce total est rejetée immédiatement avec un 529 au lieu d'attendre ; une requête sans corps n'est jamais rejetée pour cela. QUEUE_MAX_WAITING borne le même ensemble en requêtes, ce qui ne dit rien de leur taille — les corps sont mis en tampon en entier avant qu'une requête n'atteigne la file, si bien qu'avec un simple décompte pour les arrêter, l'ensemble stationné peut détenir QUEUE_MAX_WAITING × 10 Mio. Augmentez-le pour une application riche en uploads qui doit absorber les rafales de gros corps plutôt que les délester ; abaissez-le sur un conteneur à mémoire plafonnée. 0 = automatique

Dimensionner l'ensemble en attente

Le nombre de requêtes qui peuvent utilement attendre découle de la vitesse à laquelle le pool les écoule. Avec W workers et un gestionnaire qui prend T millisecondes, le pool admet W / T requêtes par milliseconde, donc un budget de B millisecondes peut laisser entrer environ W × B / T requêtes en attente. Tout ce qui dépasse attend le budget entier et est refusé de toute façon, en détenant une connexion et un corps de requête en tampon pendant tout ce temps.

Le débit de service n'est pas connu au démarrage, si bien que la valeur par défaut ne peut pas calculer cela et est délibérément généreuse. Cela convient aux gestionnaires rapides, où le pool résorbe un arriéré profond bien avant la fin du budget, et c'est beaucoup trop grand pour les lents : avec 8 workers, un gestionnaire à 200 ms et le plafond par défaut de 1 s (le budget en vigueur tant que le pool ne déleste pas), seules 40 requêtes environ peuvent être admises à temps alors que la valeur par défaut en stationne jusqu'à 1024.

bash
QUEUE_MAX_WAITING=40

Le régler près de W × B / T transforme le surplus en un 529 immédiat plutôt qu'en un 529 une seconde plus tard. Raccourcir QUEUE_WAIT_TIMEOUT_MS obtient le même résultat par l'autre terme. Les deux s'échangent l'un contre l'autre, et sur un gestionnaire lent, le budget plus court est en général le meilleur levier, car il borne aussi le temps qu'un client attend le refus. B désigne ici le budget en vigueur, qui en surcharge est celui auquel le serveur l'a raccourci plutôt que le plafond configuré. Lisez oxphp_admission_wait_budget_us avant de dimensionner l'ensemble en attente d'après un nombre tiré d'un fichier de configuration.

L'ensemble est borné une seconde fois, en octets. Chaque requête en attente garde son corps en mémoire aussi longtemps qu'elle attend, si bien qu'un plafond compté en requêtes laisse au trafic la mémoire qu'elles détiennent : les mêmes 1024 requêtes en attente ne coûtent rien sur des GET sans corps, et des gigaoctets sur des uploads. QUEUE_MAX_WAITING_BYTES borne cette somme directement — au-delà, une requête portant un corps est refusée aussitôt au lieu d'être stationnée, tandis que celles sans corps continuent d'attendre normalement. Une application riche en uploads qui doit absorber les rafales plutôt que les délester veut une valeur plus grande ; un conteneur à limite mémoire stricte en veut une plus petite. Les refus dus à l'un ou l'autre plafond sont comptés séparément dans oxphp_admission_refused_total (waiting_full pour le décompte, waiting_bytes pour la mémoire), si bien que la métrique nomme le levier à actionner.

Le budget d'attente adaptatif

QUEUE_WAIT_TIMEOUT_MS est un plafond. Tant que le travail que l'attente admet s'achève pour des clients déjà partis, le serveur raccourcit le budget de lui-même, et fortement, ce qu'il vaut mieux savoir avant de choisir un plafond :

  • À la baisse : le budget est divisé par deux chaque fois qu'un quart ou plus de ce que le pool a démarré est allé à des clients déjà partis, mesuré sur une fenêtre d'au moins huit requêtes démarrées plutôt que sur un intervalle fixe.
  • Plancher : un soixante-quatrième du plafond (15,6 ms par défaut) ou 10 ms, selon la plus grande valeur, sans jamais dépasser le plafond lui-même.
  • À la hausse : il remonte par huitièmes du plafond, et seulement tant que la file est observée vide et que moins d'un quart de ce que le pool a démarré est allé à des clients partis. Le filet ordinaire d'onglets fermés et de délais client expirés ne le maintient pas bas, et un serveur dont la file ne se vide jamais reste sur le budget raccourci jusqu'à ce que la charge retombe.

Un opérateur qui a défini 1000 doit s'attendre à ce qu'environ 15 soit en vigueur pendant un épisode ; écrivez donc les alertes sur l'attente d'après la paire oxphp_admission_wait_budget_us / oxphp_admission_wait_budget_ceiling_us plutôt que d'après le nombre configuré.

Le raccourcissement est provoqué par les clients qui ferment. Une population qui cesse d'attendre sans fermer ne produit aucun signal et laisse le plafond en vigueur.

Auto-appels

Une application qui rappelle ce même serveur en HTTP gaspille ses attentes quelle que soit leur durée : l'appel intérieur a besoin d'un worker que l'appel extérieur détient. oxphp_admission_wait_wasted_total compte ces attentes, mais seulement tant que le budget est à son plafond. Là où le budget a baissé, le compteur se tait, si bien qu'un 0 à cet endroit ne montre pas à lui seul que le motif est absent. Que le budget baisse ou non n'est pas non plus une propriété de l'auto-appel : un auto-appelant dont les propres appelants attendent généreusement (un cron interne, un sidecar au délai long) garde son plafond.

QUEUE_WAIT_TIMEOUT_MS=0 ne règle pas cela. Il supprime l'attente d'une place dans la file, pas l'attente à l'intérieur de la file, si bien qu'à moins que QUEUE_CAPACITY soit assez petit pour que la file soit pleine, l'appel intérieur est admis dans une file que rien ne consomme et ne reçoit jamais de réponse.

Garder de la marge pour la boucle d'acceptation

Une requête détient sa connexion — et l'un des permis MAX_CONNECTIONS — du moment où elle arrive jusqu'à sa réponse, quoi qu'elle fasse entre-temps. Cela couvre trois populations distinctes, car une place dans la file est libérée dès qu'un worker prend la requête en charge, avant que le script ne s'exécute :

  • en cours d'exécution dans un worker : au moins PHP_WORKERS, et davantage en mode worker, où un thread multiplexe des fibers ;
  • en file, jusqu'à QUEUE_CAPACITY ;
  • stationnées à l'admission, jusqu'à QUEUE_MAX_WAITING.

Seule la troisième a un plafond dérivé de MAX_CONNECTIONS. Gardez PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING en dessous de MAX_CONNECTIONS. Au-delà, le mode de défaillance change pour le pire : la boucle d'acceptation prend un permis de connexion avant de commencer à servir une connexion, si bien qu'une fois que le chemin PHP les détient tous, elle se met en pause, et un client qui arrive alors n'obtient aucune réponse au lieu d'un 529 — ce qu'un répartiteur de charge ne peut pas distinguer d'une instance morte, tandis que la sonde de santé sur INTERNAL_ADDR reste au vert parce qu'elle ne passe par aucune des deux limites.

Le blocage est journalisé. Une acceptation qui doit attendre un permis journalise un WARN nommant le budget, et un INFO indiquant combien de temps la boucle est restée en pause est écrit dès que le permis arrive. Ces rapports sont limités en débit pour qu'un serveur qui oscille autour du plafond ne puisse pas transformer son propre journal en panne : un blocage qui commence quelques secondes après un blocage déjà rapporté est d'abord retenu, puis rapporté à la fermeture de cette fenêtre s'il dure encore ; un blocage qui se termine dans la fenêtre ne laisse aucune ligne. Les métriques les voient tous : oxphp_accept_stalled (une jauge, lisible pendant toute la durée du blocage parce que le listener interne ne passe pas par MAX_CONNECTIONS) et oxphp_accept_stalls_total (un compteur) ; voir la référence des métriques. Les clients qui arrivent n'obtiennent toujours aucune réponse plutôt qu'une erreur, mais le journal et les métriques enregistrent le blocage.

Le serveur le vérifie au démarrage et avertit quand la somme atteint le budget, en nommant chaque valeur ; oxphp config --check rapporte la même chose, sans changer son verdict ni son code de sortie. Considérez la disparition de l'avertissement comme nécessaire plutôt que suffisante : les connexions keep-alive inactives, les requêtes de fichiers statiques et les poignées de main en cours détiennent elles aussi des permis, et aucune ne peut être comptée au démarrage.

Abaisser MAX_CONNECTIONS est la manière habituelle de déclencher l'avertissement, car la valeur par défaut de QUEUE_CAPACITY est dimensionnée d'après le nombre de workers et ne le suit pas vers le bas — 7 workers et MAX_CONNECTIONS=1000 donnent 7 + 896 + 500 face à un budget de 1000. Notez quel levier actionner : relever MAX_CONNECTIONS seul ne le fait pas disparaître tant que QUEUE_MAX_WAITING reste à sa valeur par défaut, car cette valeur vaut la moitié de MAX_CONNECTIONS et monte avec lui — à 7 workers, la somme se stabilise à 1799 et la condition ne se dissipe qu'à partir de MAX_CONNECTIONS=1800. Abaissez QUEUE_CAPACITY, ou définissez QUEUE_MAX_WAITING explicitement puis relevez le budget.

Pour un pool dynamique (MIN:MAX), le terme en cours d'exécution est le minimum, le même nombre qui dimensionne les valeurs par défaut de la file, si bien qu'un pool monté à son maximum détient plus que ce que la somme annonce.

La comparaison compte des connexions alors que le budget se dépense en requêtes, si bien qu'un déploiement à dominante HTTP/2 peut légitimement se situer au-dessus : une connexion transporte jusqu'à H2_MAX_CONCURRENT_STREAMS requêtes, le même arriéré est donc détenu par une fraction des connexions. L'avertissement y est attendu. Un grand pool avec le budget d'origine l'atteint aussi — un pool auto-dimensionné de 39 workers donne 39 + 4992 + 4992 face à 10 000 — et celui-là mérite d'être traité plutôt qu'ignoré.

Workers statiques ou dynamiques

Définissez PHP_WORKERS sur un seul nombre pour un pool fixe :

bash
PHP_WORKERS=8 # Fixed 8 workers PHP_WORKERS=0 # Auto-detect: CPU / 2 (min 1)

Définissez PHP_WORKERS sur MIN:MAX pour une mise à l'échelle automatique :

bash
PHP_WORKERS=2:16 # Scale between 2 and 16 workers PHP_WORKERS=4:0 # 4 minimum, auto-detect maximum (CPU × 2) PHP_WORKERS=0:16 # auto-detect minimum (CPU / 4, min 1), 16 maximum

En mode dynamique, OxPHP augmente le nombre de workers lorsqu'ils sont tous occupés et le réduit lorsque des workers sont restés inactifs plus longtemps que PHP_WORKERS_IDLE_SECONDS.

Comment le nombre de CPU est détecté

Chaque valeur par défaut de cette page exprimée en CPU (TOKIO_WORKERS, et PHP_WORKERS non défini, à 0, ou avec 0 de part ou d'autre de MIN:MAX) compte les CPU sur lesquels le processus est autorisé à s'exécuter : son masque d'affinité (taskset, docker run --cpuset-cpus), abaissé au quota CPU du conteneur lorsqu'il y en a un (docker run --cpus ; cpu.max en cgroup v2, cpu.cfs_quota_us en v1). Un quota est arrondi à l'inférieur et compte pour au moins 1, si bien que --cpus=1.5 compte pour 1.

Deux types de limite ne sont pas comptés, et un conteneur bridé par l'une d'elles seulement est dimensionné d'après tous les CPU qu'il voit :

  • Un poids CPU : docker run --cpu-shares, cpu.weight en cgroup v2, cpu.shares en v1. Il décide comment le temps CPU disputé est partagé avec les voisins. Ce n'est pas un plafond, et aucun nombre de CPU n'en découle.
  • Une limite appliquée hors du conteneur. Une plateforme peut vendre une part d'une machine et appliquer la limite là où le conteneur ne peut pas la voir. Sur DigitalOcean App Platform, par exemple, une instance vendue comme 1 vCPU a été observée en train de tourner dans un bac à sable gVisor sans quota (cpu.cfs_quota_us vaut -1) et avec 8 CPU visibles, si bien que les valeurs par défaut aboutissaient à 4 workers PHP, 4 threads de runtime et une file de 512.
Offres qui appliquent leurs limites hors de vue

Sur une telle offre, définissez explicitement PHP_WORKERS et TOKIO_WORKERS selon ce que l'offre fournit réellement. En mode worker, confrontez aussi PHP_WORKERS × WORKER_MAX_MEMORY_MIB à la mémoire de l'offre. Ce seuil est un déclencheur de recyclage que le worker vérifie dans sa boucle, pas un plafond sur ce que son tas PHP peut allouer : le pool peut donc détenir autant, et davantage, avant qu'un worker ne soit recyclé pour cette raison, avec OPcache et le serveur lui-même en plus ; avec la valeur par défaut 0, il n'y a aucun seuil. Le même bac à sable peut aussi masquer la limite de mémoire : le conteneur App Platform ci-dessus n'en voyait aucune.

Au démarrage, le serveur journalise le nombre, ce qui l'a fixé, et les PHP_WORKERS et TOKIO_WORKERS qui en résultent. Voici ce que cela donne sous docker run --cpus=1 sur une machine à 14 CPU :

json
{"timestamp":"2026-10-05T18:55:39.801254Z","level":"INFO","fields":{"message":"CPU count for default sizing","cpus":1,"cpu_source":"cgroup quota","host_cpus":14,"php_workers":"1 (auto)","tokio_workers":"1 (auto)"}}
  • cpu_source vaut host quand ni un masque ni un quota n'ont abaissé le nombre, affinity ou cgroup quota quand l'un d'eux l'a fait, et unknown quand le serveur n'a pas pu trancher parce que le masque ou le nombre de CPU en ligne n'a pas pu être lu. S'il n'a pas pu compter les CPU du tout, cpus vaut 4.
  • host_cpus est le nombre de CPU que le système déclare en ligne (/sys/devices/system/cpu/online sous Linux), qu'un masque ou un quota n'abaisse pas. Il est omis quand il ne peut pas être lu. Un runtime qui réécrit ce fichier pour n'y lister que les CPU du conteneur fait lire un masque restreint comme host.
  • (auto) marque une valeur laissée entièrement à la valeur par défaut : la variable n'est pas définie ou est vide, ou, pour TOKIO_WORKERS, n'est pas un nombre.

Un cpu_source à host sur une offre qui vous a vendu moins de CPU que cpus, c'est le cas décrit plus haut.

Mode worker

Variable Valeur par défaut Description
WORKER_MAX_MEMORY_MIB 0 Mémoire maximale en MiB par worker avant recyclage. 0 = illimité

Définissez WORKER_MODE_ENABLED=true et faites pointer ENTRY_FILE vers votre script d'amorçage de worker (par exemple ENTRY_FILE=worker.php ou ENTRY_FILE=../worker.php). Les workers PHP restent alors en vie d'une requête à l'autre, en gardant en mémoire l'état d'amorçage (autoloaders, connexions à la base de données). Les workers sont recyclés automatiquement lorsqu'ils dépassent WORKER_MAX_MEMORY_MIB ou reçoivent trois requêtes consécutives qui se sont mal terminées, et à la demande lorsque l'application appelle Worker::scheduleExit() ; voir Recyclage pour ce qui compte dans ce troisième cas. Le réglage WORKER_MAX_REQUESTS des versions antérieures est déprécié et ignoré : ne définissez ni l'un ni l'autre, ou migrez vers Worker::scheduleExit().

Déprécié : INDEX_FILE et WORKER_FILE

Les variables héritées INDEX_FILE et WORKER_FILE sont toujours analysées à des fins de rétrocompatibilité. Lorsqu'elles sont définies, elles émettent une ligne de journal WARN au démarrage et se traduisent dans le nouveau modèle :

Héritée Équivalent actuel
INDEX_FILE=index.php ENTRY_FILE=index.php
INDEX_FILE=index.html ENTRY_FILE=index.html
WORKER_FILE=/path/worker.php WORKER_MODE_ENABLED=true ENTRY_FILE=/path/worker.php

Si l'ancienne et la nouvelle sont toutes deux définies, ENTRY_FILE / WORKER_MODE_ENABLED l'emportent. Migrez à votre convenance ; les formes dépréciées seront supprimées dans une version future.

SAPI / PHP

Variable Valeur par défaut Description
SUPERGLOBALS_ENABLED true Décrit la requête à PHP pour qu'il construise $_SERVER et $_GET avant l'exécution du script. Définissez sur une valeur fausse pour s'épargner ce travail dans une application qui ne lit ni l'une ni l'autre. Cela ne concerne que ces deux-là : $_POST, $_FILES et $_COOKIE sont construits à partir du corps de la requête et de l'en-tête Cookie, qui sont remis à PHP dans tous les cas, et php://input est un flux plutôt qu'une superglobale. Voir SUPERGLOBALS_ENABLED pour le tableau complet

Délais d'expiration

Variable Valeur par défaut Description
HEADER_TIMEOUT_SECONDS 5 Nombre maximal de secondes pour recevoir les en-têtes HTTP après la connexion (protection contre Slowloris)
DRAIN_TIMEOUT_SECONDS 25 Nombre maximal de secondes d'attente des connexions en cours lors d'un arrêt gracieux. Une requête prise dans un appel bloquant à l'échéance peut retenir le processus au-delà (voir Arrêt gracieux)

Le temps d'exécution PHP est borné par la directive ini max_execution_time de PHP (et set_time_limit() à l'exécution), et non par une variable d'environnement OxPHP. Cette borne n'atteint pas une tâche asynchrone : un thread du pool ASYNC_WORKERS ne détient aucune échéance d'exécution, si bien que ni la directive ni set_time_limit() ne limitent ce qu'exécute une tâche envoyée avec oxphp_async(). Bornez plutôt une telle tâche avec l'argument de délai d'oxphp_async_await() ; voir Promesses asynchrones.

Limitation de débit

Variable Valeur par défaut Description
RATE_LIMIT 0 (désactivé) Nombre maximal de requêtes par IP et par fenêtre de temps. 0 désactive la limitation de débit
RATE_WINDOW_SECONDS 60 Durée de la fenêtre de limitation de débit en secondes

Sécurité

Variable Valeur par défaut Description
FRAME_OPTIONS SAMEORIGIN Protection contre le clickjacking. SAMEORIGIN n'autorise le framing que par des pages de la même origine, DENY bloque tout affichage en cadre (framing), off le désactive (à utiliser lorsque vous gérez le framing via votre propre CSP). Toute autre valeur retombe sur la valeur par défaut SAMEORIGIN avec un avertissement au démarrage. Définit à la fois X-Frame-Options et Content-Security-Policy: frame-ancestors sur chaque réponse. Voir Protection contre le clickjacking ci-dessous pour les valeurs d'en-tête émises, la manière dont les en-têtes serveur s'effacent devant ceux définis par l'application, et des conseils pour choisir une valeur
TRUSTED_PROXIES (non défini) Réseaux de proxys inverses de confiance : un mélange, séparé par des virgules, de CIDR, d'IP nues et des mots-clés private et cloudflare (par exemple private,cloudflare). Lorsqu'elle est définie, OxPHP extrait la véritable IP du client à partir des en-têtes Forwarded (RFC 7239) ou X-Forwarded-For en appliquant l'algorithme rightmost-non-trusted (le nœud non fiable le plus à droite) ; avec cloudflare, une requête dont la chaîne de transfert montre qu'elle est arrivée depuis un nœud de bordure Cloudflare la tire plutôt de CF-Connecting-IP. Traite également X-Forwarded-Proto et X-Forwarded-Host pour $_SERVER['HTTPS'], REQUEST_SCHEME, SERVER_NAME et SERVER_PORT. Non défini = fonctionnalité désactivée
PHP_DENY_PATHS (non défini) Motifs glob séparés par des virgules dont les fichiers .php ne doivent jamais s'exécuter via un URI direct (par exemple /uploads/**,/cache/**,/admin/legacy.php). Les motifs peuvent cibler des répertoires entiers ou des fichiers isolés. S'applique dans les modes de mappage direct — Traditionnel et SPA ; ignoré avec un avertissement au démarrage dans les modes Framework et Worker, qui n'exécutent jamais directement des fichiers .php arbitraires. Couvre aussi les scripts atteints via la résolution d'index de répertoire (/uploads/ → uploads/index.php). Pour les URI .php directs, la correspondance a lieu avant toute E/S disque, si bien que les chemins refusés produisent la même réponse que le fichier existe ou non (pas d'oracle d'existence). L'ancien nom PHP_DENY_DIRS est accepté comme alias déprécié et émet un WARN au démarrage. Voir Liste de refus d'exécution PHP
PHP_DENY_FALLBACK 404 Ce qui est renvoyé en cas de correspondance avec PHP_DENY_PATHS ou avec une règle de refus .oxphpdeny. Soit un statut HTTP 400–599 (associé à ERROR_PAGES_DIR pour un HTML personnalisé), soit un chemin d'URI préfixé par / vers un script PHP de repli situé dans DOCUMENT_ROOT. Le script de repli reçoit OXPHP_DENIED_PATH et OXPHP_DENIED_PATTERN dans $_SERVER. Validé au démarrage : le script doit exister, se canonicaliser à l'intérieur de DOCUMENT_ROOT, et ne doit pas lui-même correspondre à PHP_DENY_PATHS (prévention des boucles). Une règle .oxphpdeny peut couvrir le script ; en mode worker, les règles de refus .oxphpdeny n'acceptent qu'un code de statut. Voir Règles de refus de chemins
SYMLINK_ALLOW_PATHS (non défini) Liste, séparée par des virgules, de chemins absolus sous lesquels les liens symboliques sont autorisés à sortir de DOCUMENT_ROOT. Chaque entrée doit déjà exister sur le disque ; les chemins relatifs et les chemins manquants interrompent le démarrage. Non défini = aucune sortie par lien symbolique autorisée. Voir Liste d'autorisation de liens symboliques

La valeur spéciale private se développe en tous les réseaux privés RFC-1918, l'adresse de bouclage (loopback) et les adresses link-local (IPv4 et IPv6) : 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16, ::1/128, fc00::/7, fe80::/10.

Protection contre le clickjacking

Le clickjacking est une attaque où une page hostile intègre votre site dans une <iframe> invisible et pousse l'utilisateur à cliquer sur quelque chose qu'il ne voit pas : un bouton « oui, supprimer mon compte », un achat en un clic, une invite « autoriser » OAuth. La défense consiste à dire au navigateur qui, le cas échéant, a le droit d'encadrer vos pages. FRAME_OPTIONS contrôle cela.

Deux en-têtes gouvernent le framing — l'historique X-Frame-Options (compris par tous les navigateurs) et le moderne Content-Security-Policy: frame-ancestors (qui le supplante là où les deux sont présents) — OxPHP émet donc les deux, et la politique tient sur les navigateurs anciens comme récents. Une seule valeur de FRAME_OPTIONS correspond à une paire assortie :

FRAME_OPTIONS X-Frame-Options Content-Security-Policy Qui peut encadrer vos pages
SAMEORIGIN (par défaut) SAMEORIGIN frame-ancestors 'self' Uniquement les pages de la même origine
DENY DENY frame-ancestors 'none' Personne, pas même vos propres pages
off (non envoyé) (non envoyé) N'importe qui — aucune restriction de framing côté serveur

Choisir une valeur. SAMEORIGIN est la valeur par défaut : elle bloque le framing inter-origines sur lequel le clickjacking repose réellement, tout en laissant vos propres pages s'intégrer les unes dans les autres — ce que beaucoup d'applications font légitimement (aperçus d'admin, widgets de tableau de bord, composants de paiement hébergés sur la même origine). Choisissez DENY quand rien sur votre site n'est jamais censé être encadré, pas même par lui-même, pour la posture la plus stricte. Choisissez off uniquement quand vous gérez le framing vous-même via une Content-Security-Policy complète que votre application définit — voir ci-dessous.

Encadrer depuis des origines externes. Aucune valeur de X-Frame-Options ne peut nommer une origine autorisée précise (ALLOW-FROM a été retiré du standard). Pour permettre à un tiers nommé d'encadrer vos pages, définissez FRAME_OPTIONS=off et faites émettre à votre application sa propre Content-Security-Policy avec une liste frame-ancestors explicite, par exemple header("Content-Security-Policy: frame-ancestors 'self' https://partner.example.com");.

Les en-têtes applicatifs l'emportent. Les en-têtes serveur sont des replis, appliqués uniquement quand la réponse ne porte pas déjà un tel en-tête : une application qui définit son propre X-Frame-Options ou sa propre Content-Security-Policy via header() en PHP les conserve intacts. Les deux en-têtes de framing sont traités comme une seule politique, si bien que le serveur ne contredit jamais l'application :

  • Si votre application définit X-Frame-Options, OxPHP omet son repli frame-ancestors (une CSP serveur écraserait le choix de votre application dans les navigateurs modernes).
  • Si votre application définit une Content-Security-Policy contenant une directive frame-ancestors, OxPHP omet son repli X-Frame-Options (un X-Frame-Options serveur plus strict sur-bloquerait dans les navigateurs anciens qui ignorent la CSP).

La même priorité s'applique à X-Content-Type-Options, qu'OxPHP définit à nosniff sur chaque réponse : une valeur définie par l'application est conservée telle quelle. Notez que nosniff est la seule valeur qui fasse quelque chose — une application qui la remplace par autre chose désactive silencieusement la protection contre le MIME-sniffing.

TLS

Variable Valeur par défaut Description
TLS_CERT (non défini) Chemin vers le certificat TLS encodé en PEM. TLS_CERT et TLS_KEY doivent tous deux être définis pour activer TLS
TLS_KEY (non défini) Chemin vers la clé privée TLS encodée en PEM
TLS_MIN_VERSION 1.2 Version minimale acceptée du protocole TLS : 1.2 ou 1.3. Validée au démarrage (et par oxphp config --check) même lorsque TLS n'est pas activé — toute autre valeur, y compris des octets non-UTF-8, constitue une erreur de démarrage fatale. Une valeur vide est traitée comme non définie

HTTP/2

Variable Valeur par défaut Description
H2_MAX_CONCURRENT_STREAMS PHP_WORKERS_MAX × 4 (min. 32) Nombre maximal de flux ouverts simultanément par connexion HTTP/2
H2_MAX_PENDING_RESET 20 Nombre maximal de trames RST_STREAM en file d'attente avant fermeture d'une connexion (protection contre Rapid Reset)
H2_MAX_HEADER_LIST_BYTES 65536 Nombre maximal total d'octets d'en-tête décodés par requête
H2_KEEPALIVE_INTERVAL_SECS 20 Nombre de secondes entre les trames PING HTTP/2 ; 0 désactive
H2_KEEPALIVE_TIMEOUT_SECS 10 Nombre de secondes d'attente d'une réponse PING avant de fermer la connexion

Fichiers statiques

Variable Valeur par défaut Description
STATIC_MAX_AGE 30d Cache-Control: max-age pour les fichiers statiques. Accepte : 30s, 5m, 2h, 30d, 1w, 1y, un nombre de secondes brut (3600), ou off pour désactiver l'en-tête. Remplace STATIC_CACHE_TTL, déprécié.
STATIC_REVALIDATE off Booléen — voir Valeurs booléennes. Définissez sur une valeur vraie pour activer la revalidation par mtime du cache de contenu en mémoire : la date de modification du fichier est revérifiée au plus une fois toutes les 3 secondes par fichier (pas par requête) et les entrées périmées sont évincées automatiquement, si bien que les changements deviennent visibles en moins de 3 secondes. Remplace STATIC_CACHE, déprécié (où off avait le sens inverse).

Compression

Variable Valeur par défaut Description
COMPRESSION_ENCODINGS br,zstd,gzip Codages de contenu proposés par le serveur, séparés par des virgules : br (ou brotli), zstd, gzip. Un nom inconnu est une erreur au démarrage. Définissez la valeur entière à off (ou none) pour désactiver complètement la compression. Ce n'est pas un élément de liste, si bien que br,off est une erreur plutôt qu'un interrupteur. Une affectation vide rétablit la valeur par défaut. L'ordre écrit ici est ignoré : les fichiers statiques en cache préfèrent Brotli, dont le coût n'est payé qu'une fois, et tout le reste préfère Zstandard. Voir Compression
COMPRESSION_BROTLI_LEVEL 5 Qualité Brotli (0 à 11). Remplace COMPRESSION_LEVEL, déprécié
COMPRESSION_ZSTD_LEVEL 6 Niveau de compression Zstandard (0 à 19)
COMPRESSION_GZIP_LEVEL 6 Niveau de compression Gzip (0 à 9)
COMPRESSION_LEVEL (non défini) Ancien nom, déprécié, de COMPRESSION_BROTLI_LEVEL ; émet un WARN au démarrage. COMPRESSION_LEVEL=0 garde son sens d'origine et désactive tous les codages, pas seulement Brotli. Un COMPRESSION_BROTLI_LEVEL explicite l'emporte sur une valeur non nulle, mais pas sur 0, qui désactive la compression quoi que soit défini par ailleurs

Un codage est proposé quand COMPRESSION_ENCODINGS le liste et que son niveau n'est pas 0 ; l'une ou l'autre condition seule suffit à le retirer. Un niveau hors de sa plage, ou qui n'est pas un nombre, interrompt le démarrage avec une erreur nommant la variable. Les valeurs ne sont pas ramenées dans les bornes.

Journalisation

Variable Valeur par défaut Description
LOG_LEVEL info Verbosité des journaux : trace, debug, info, warn, error
ACCESS_LOG (non défini) Journal d'accès par requête : all = chaque requête, error = 4xx/5xx uniquement, non défini = désactivé
Note

ACCESS_LOG accepte all ou error. Laissez la variable non définie pour désactiver totalement la journalisation des accès.

Un client qui ferme sa connexion avant que le serveur ait répondu est journalisé au niveau debug, pas error. C'est l'événement le plus courant que voit un listener public (un fetch annulé, un onglet fermé, un proxy dont le délai de lecture a expiré) et ce n'est pas une défaillance du serveur ; au LOG_LEVEL par défaut, il est donc invisible. Comptez-les avec oxphp_request_cancelled_total{reason="client_abort"}, ou passez à LOG_LEVEL=debug pour voir les connexions individuelles. Les délais que le serveur applique lui-même (HEADER_TIMEOUT_SECONDS) restent au niveau warn, et toutes les autres défaillances de connexion restent au niveau error.

LOG_LEVEL ne filtre pas le journal d'accès

Les deux sont indépendants. LOG_LEVEL règle la verbosité des diagnostics propres au serveur, si bien que LOG_LEVEL=warn avec ACCESS_LOG=error rapporte les avertissements et les erreurs et journalise chaque requête 4xx/5xx. Faire taire le journal d'accès seul demande une directive RUST_LOG nommant sa cible : RUST_LOG=warn,access_log=off, où warn tient lieu du LOG_LEVEL que vous utilisiez. RUST_LOG, lorsqu'il est défini et analysable, remplace LOG_LEVEL au lieu de s'y ajouter. Voir Journalisation des accès.

Observabilité

Variable Valeur par défaut Description
INTERNAL_ADDR (non défini) Adresse du serveur interne (/health, /metrics, /config). Le serveur interne n'est pas démarré lorsque cette variable n'est pas définie. Une valeur ne comportant qu'un port (:9090) se lie à 127.0.0.1 ; liez explicitement 0.0.0.0:9090 pour l'exposer hors de l'hôte
INTERNAL_ALLOW_IPS (non défini) Liste d'autorisation CIDR/IP, séparée par des virgules, pour le serveur interne. Un pair extérieur à la liste reçoit 403 sur /metrics, /config et les chemins de plugins ; les sondes de santé (/health, /healthz, /readyz, /startupz et leurs formes longues) restent accessibles. Non défini/vide = tout est autorisé. Le bouclage (loopback) n'est pas implicite — listez 127.0.0.1/32 pour conserver l'accès depuis localhost. Une liste mal formée interrompt le démarrage
ERROR_PAGES_DIR (non défini) Répertoire contenant les pages d'erreur personnalisées nommées {status}.html (par exemple 404.html, 503.html)
MAX_QUERY_BODY 524288 Taille maximale du corps de requête, en octets, pour les endpoints de requête internes (512 KiB)
TRACE_CONTEXT false Booléen — voir Valeurs booléennes. Lorsque la valeur est vraie, active la propagation du W3C Trace Context : lit les en-têtes traceparent/tracestate et les transmet à PHP via $_SERVER

OpenTelemetry

Variable Valeur par défaut Description
OTEL_ENABLED false Active l'export de spans OpenTelemetry. Définit automatiquement TRACE_CONTEXT=true. Booléen — voir Valeurs booléennes
OTEL_EXPORTER_OTLP_PROTOCOL grpc Protocole d'export : grpc ou http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT http://localhost:4317 (gRPC) ou http://localhost:4318 (HTTP) Endpoint du collecteur OTLP. Avec http/protobuf, une URL de base : les spans sont envoyés à /v1/traces sous celle-ci
OTEL_EXPORTER_OTLP_TIMEOUT 10000 Délai d'expiration de l'export en millisecondes. En gRPC, il borne aussi la connexion au collecteur, poignée de main TLS comprise : gardez-le donc au-dessus du temps que cela prend ; une tentative qui doit d'abord se connecter peut durer jusqu'à deux fois ce délai. En gRPC sous Linux, il fixe aussi au bout de combien de temps une connexion qui a cessé d'accepter des données est fermée — voir Erreurs d'export de spans dans le journal
OTEL_EXPORTER_OTLP_HEADERS (non défini) En-têtes d'authentification : key=value,key2=value2
OTEL_SERVICE_NAME oxphp Nom du service dans les spans exportés
OTEL_SERVICE_VERSION (non défini) Attribut de version du service
OTEL_RESOURCE_ATTRIBUTES (non défini) Attributs de ressource supplémentaires : env=prod,region=us-east-1
OTEL_TRACES_SAMPLER parentbased_traceidratio Stratégie d'échantillonnage : always_on, always_off, traceidratio, parentbased_always_on, parentbased_always_off, parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG 1.0 Taux d'échantillonnage (0.0–1.0) pour les échantillonneurs basés sur un ratio
Note

Les valeurs OTEL_TRACES_SAMPLER_ARG invalides ou hors plage sont ramenées à [0.0, 1.0] et journalisées au niveau warn. Les valeurs OTEL_TRACES_SAMPLER inconnues retombent sur parentbased_traceidratio et sont journalisées.

APM

Variable Valeur par défaut Description
OTEL_APM_ENABLED false Active l'APM : instrumentation automatique, capture des erreurs et SDK de traçage PHP. Nécessite OTEL_ENABLED=true. Booléen — voir Valeurs booléennes
OTEL_APM_SLOW_QUERY_MS 100 Seuil de requête lente en millisecondes. Les requêtes de base de données qui le dépassent reçoivent un attribut de span oxphp.db.slow=true
OTEL_APM_DB_CAPTURE_PARAMS_ENABLED false Enregistre les paramètres liés (bind) dans l'attribut de span db.params. À désactiver en production si les paramètres peuvent contenir des données sensibles. Booléen — voir Valeurs booléennes
OTEL_APM_STACKTRACE_MAX_BYTES 8192 Taille maximale, en octets, de l'attribut exception.stacktrace. Au-delà du plafond, la trace d'appels est tronquée par la fin avec un marqueur …(truncated). 0 désactive la troncature
OTEL_APM_MESSAGE_MAX_BYTES 4096 Taille maximale, en octets, de l'attribut exception.message (la valeur par défaut correspond à la limite de valeur par attribut de New Relic). Au-delà du plafond, le message est tronqué par la fin avec un marqueur …(truncated). 0 désactive la troncature

Lorsque l'APM est activé, OxPHP intercepte automatiquement 34 fonctions PHP internes (PDO, mysqli, cURL, Redis, Memcached, E/S sur fichiers) pour créer des spans enfants. Les fonctions PHP oxphp_apm_*() sont enregistrées que l'APM soit activé ou non. Lorsqu'il est désactivé, les fonctions de span sont des no-ops qui renvoient des valeurs sentinelles. oxphp_apm_trace() fait exception : elle exécute toujours son callback et renvoie la valeur de celui-ci, si bien que désactiver l'APM ne change jamais ce que calcule votre application.

Workers asynchrones

Variable Valeur par défaut Description
ASYNC_WORKERS 0 (désactivé) Nombre de threads de workers asynchrones dédiés. Lorsque la valeur est 0, les fonctions asynchrones (oxphp_async, etc.) sont enregistrées mais lèvent OxPHP\Async\AsyncException à l'appel. Définissez une valeur positive pour activer l'exécution de tâches en arrière-plan
ASYNC_QUEUE_CAPACITY ASYNC_WORKERS × 64 Nombre maximal de tâches en attente dans la file asynchrone. 0 = automatique (workers × 64)
ASYNC_MAX_FIBERS 256 Plafond, par worker, du nombre de Fibers de tâches asynchrones concurrentes. La limite globale du processus (en attente + en cours d'exécution) est ASYNC_MAX_FIBERS × ASYNC_WORKERS ; un envoi au-delà est rejeté immédiatement avec OxPHP\Async\AsyncException, si bien qu'une composition en fan-out ne peut pas provoquer d'interblocage

Le pool de workers asynchrones traite les tâches d'arrière-plan de type « fire-and-forget » envoyées depuis PHP. Il est distinct du pool de workers PHP et n'est pas nécessaire au traitement standard des requêtes.

Une valeur mal formée dans l'une de ces trois variables (par exemple ASYNC_WORKERS=8x) constitue une erreur de démarrage — retomber sur une valeur par défaut désactiverait ou mal configurerait silencieusement le pool. Une valeur exactement vide est traitée comme non définie.

Hooks d'exécution

Variable Valeur par défaut Description
RUNTIME_HOOKS (désactivé) Remplacement opt-in de fonctions natives PHP bloquantes par des implémentations qui suspendent la Fiber. 1/true/all active toutes les catégories de hooks ; une liste séparée par des virgules active des catégories précises (par exemple RUNTIME_HOOKS=sleep,streams)
Catégorie Ce qu'elle intercepte
sleep Les fonctions natives sleep() et usleep() suspendent la Fiber courante exactement comme oxphp_sleep()/oxphp_usleep(), y compris dans les contextes où ces deux-là ne peuvent pas suspendre non plus et attendent sur le thread worker à la place (voir Où les hooks sleep ne suspendent pas)
streams Deux attentes suspendent la Fiber courante au lieu d'épingler le thread worker : une lecture bloquante sur un flux socket tcp://, et stream_select(). La lecture couvre les clients qui bloquent sur un seul socket — fsockopen(), stream_socket_client(), les wrappers de flux HTTP, mysqlnd (PDO_MySQL, mysqli), phpredis ; stream_select() couvre les boucles qui attendent sur plusieurs sockets à la fois. Aucune modification de code dans les deux cas. Les clients qui attendent d'une autre manière ne sont pas concernés (voir ci-dessous)
net Les attentes situées sous les flux PHP, là où le hook de lecture streams ne peut pas les voir, suspendent la Fiber courante au lieu d'épingler le thread worker : l'établissement de connexion via stream_socket_client(), fsockopen() et tous les clients bâtis sur les flux PHP (mysqlnd, donc PDO_MySQL et mysqli, et phpredis parmi eux), la poignée de main TLS, les lectures sur les flux ssl:///tls:// une fois le chiffrement actif, les écritures qui doivent attendre de la place dans le tampon d'envoi, et les lectures de flux sur les sockets unix://, udp:// et udg://, sur les flux stream_socket_pair(), et sur les flux que socket_export_stream() crée à partir de sockets qui ne sont pas TCP. Nécessite que streams soit listé à côté, et ne fonctionne que sous Linux (voir Quand net reste désactivé). Non couverts : les attentes dans libcurl, libpq (pgsql, pdo_pgsql) et libmemcached, qui appellent poll() depuis leurs propres bibliothèques ; la résolution DNS ; stream_socket_recvfrom() ; les écritures sur un flux en clair (non TLS) avec un délai infini (default_socket_timeout=-1) ; et les établissements de connexion persistants (pfsockopen(), STREAM_CLIENT_PERSISTENT, et les modes persistants des clients bâtis sur eux)

Les hooks prennent effet dans les Fibers de requête du mode worker et dans les Fibers de tâches asynchrones. Hors d'une Fiber (contexte de requête traditionnel/framework/SPA, CLI), le comportement natif d'origine est préservé, y compris les erreurs de validation d'arguments. Avec les hooks sleep activés, du code tiers appelant sleep() cesse d'épingler le thread worker partout où la Fiber peut être suspendue (ce qui n'est pas partout ; voir ci-dessous), sans aucune modification de code. Annuler une tâche asynchrone pendant un sleep intercepté la déroule avec OxPHP\Async\AsyncException, et un sleep() intercepté qui suspend renvoie 0 ; la valeur de retour d'interruption par signal de la fonction native ne survient que sur le chemin bloquant ci-dessous.

Vérifier que le réglage a été pris en compte

Le serveur rapporte les catégories qu'il a installées, sous forme d'ensemble analysé, à deux endroits. Sur le serveur interne :

Installed hook categories
$ curl -s localhost:9090/config | jq .runtime_hooks [ "sleep", "streams", "net" ]

Et depuis PHP, pour un déploiement dont le serveur interne n'est pas joignable ou pas activé :

php
<?php echo json_encode(oxphp_server_info()['runtime_hooks']); // ["sleep","streams","net"]

Les deux lisent la même valeur et rapportent l'ensemble plutôt que la chaîne : RUNTIME_HOOKS=all, RUNTIME_HOOKS=1 et RUNTIME_HOOKS=sleep, streams, net reviennent tous sous la forme ["sleep","streams","net"]. Une liste vide signifie qu'aucun hook n'est installé. C'est la réponse pour la variable non définie, pour RUNTIME_HOOKS=0, pour une valeur ne nommant que des catégories que ce build ne connaît pas, et pour un nom de variable mal orthographié. Une valeur qui mélange les deux est différente : RUNTIME_HOOKS=sleep,bogus se lit ["sleep"], si bien qu'une liste non vide ne prouve pas à elle seule que chaque nom a été compris. Chaque nom non reconnu obtient sa propre ligne dans le journal du serveur au démarrage, et cette ligne est le seul endroit où ils sont nommés. Vérifiez après tout changement du réglage : une variable d'environnement non reconnue n'est une erreur nulle part, si bien qu'une faute de frappe ne se manifeste sinon que par de la latence.

Installé ne veut pas dire en vigueur

La liste indique quelles fonctions natives ont vu leurs handlers remplacés, ce qui se produit au démarrage quel que soit le mode de routage. Là où il n'y a aucune Fiber, les remplacements délèguent aux handlers qu'ils ont remplacés, si bien qu'un serveur en mode traditionnel rapporte ["sleep","streams","net"] tout en se comportant nativement de bout en bout. Un processus CLI php rapporte ["sleep","streams"] : le moteur fait partie de cet exécutable, si bien que net ne peut pas distinguer les attentes de PHP de celles du programme lui-même et reste désactivé. Lisez la liste comme « ce processus est préparé pour les hooks », et le mode de routage comme l'indication que quelque chose peut ou non s'en servir.

Au sein de streams, c'est plus étroit encore : la partie lecture de socket peut échouer à s'installer à elle seule, sur une plateforme où la table d'opérations de flux ne peut pas être rendue inscriptible, tandis que stream_select() et les points d'entrée de base de données restent interceptés. La catégorie est rapportée dans les deux cas ; le seul signal est une ligne du journal du serveur indiquant que les lectures de socket restent bloquantes. Cette ligne, comme celle des catégories non reconnues et les lignes qu'écrit net (ci-dessous), suit la journalisation des erreurs de PHP : avec error_log non défini (la valeur par défaut), elle atteint le journal propre du serveur en tant que warn, et LOG_LEVEL=error la masque.

Quand net reste désactivé

net n'est rapporté que lorsqu'il est réellement en place. Listé sans streams, il reste désactivé, avec une ligne du journal du serveur qui le dit. Sur une plateforme qui n'est pas Linux, là où le code propre du serveur ne peut pas être distingué de celui de PHP au démarrage, ou là où le poll() de PHP n'est pas lié à celui que le serveur intercepte, il reste désactivé avec une ligne indiquant que les établissements de connexion et les attentes TLS restent bloquants. Un net absent de la liste alors que vous l'avez demandé est donc la réponse, et non une lacune du rapport.

La seule chose que la liste ne vous dit pas, c'est où vit OpenSSL. Les attentes qu'effectue OpenSSL (la poignée de main TLS, y compris celle qui termine l'établissement d'une connexion ssl://, ainsi que les lectures et écritures sur un flux TLS une fois le chiffrement actif) ne sont stationnées que lorsqu'OpenSSL est compilé dans PHP, comme c'est le cas dans les images officielles. Avec OpenSSL chargé comme extension partagée distincte (extension=openssl), ces attentes ne sont pas celles de PHP pour autant que le serveur puisse en juger : elles gardent donc le thread, tandis que les établissements de connexion, unix:// et les autres lectures suspendent toujours, et que net est toujours rapporté.

Où les hooks sleep ne suspendent pas

Là où un changement de Fiber est refusé, les sleep()/usleep() interceptés remettent l'appel à la fonction native qu'ils ont remplacée, qui attend sur le thread worker et renvoie ce qu'elle renvoie. Un sleep() interrompu par un signal à cet endroit rend donc les secondes qu'il n'a pas dormies, comme il le fait hors d'une Fiber. (La vérification de l'argument est celle du hook dans les deux cas : elle rejette toute valeur supérieure à 4294967295, que la fonction native non interceptée accepte et tronque.) Le changement de contexte est bloqué partout où quitter le frame courant serait dangereux, et la liste ci-dessous n'est pas exhaustive :

  • dans un handler declare(ticks) et dans la distribution des signaux de pcntl (ces deux cas relèvent de PHP lui-même) ;
  • pendant la construction de l'entrée d'une requête ;
  • dans un FILTER_CALLBACK atteint depuis filter_input_array() avec un tableau de définitions par champ (voir la note sous $_GET dans Superglobales) ;
  • dans les parcours de démontage qui exécutent les destructeurs après qu'une requête s'est mal terminée (après une erreur fatale, ou après une écriture vers une requête déjà annulée), si bien qu'un __destruct() qui dort pour temporiser le fait en détenant le thread.

La fin ordinaire d'une requête n'en fait pas partie : les destructeurs et les fonctions d'arrêt s'y exécutent avec le changement de contexte autorisé. oxphp_sleep() et oxphp_usleep() prennent eux aussi leur chemin bloquant dans chacun de ces endroits, si bien que s'en servir ne permet pas de les contourner.

Une Fiber démarrée par un ordonnanceur en espace utilisateur (AMPHP, Revolt, ou un simple new Fiber) est un cas différent : le changement de contexte n'y est pas bloqué, et l'ordonnanceur suspend et reprend ses propres Fibers normalement, mais OxPHP ne peut pas reprendre un contexte qui ne lui appartient pas, si bien que le hook s'efface pour la même raison que le hook streams. Il s'agit là d'une Fiber démarrée à l'intérieur d'une requête ; une requête en mode worker est elle-même une Fiber pilotée par OxPHP, si bien que les hooks s'y appliquent normalement. Le code à l'intérieur d'une Fiber que l'application a démarrée elle-même dort sur le thread worker, et cette part de la charge doit être dimensionnée comme si le hook était désactivé.

Ce que couvre le hook streams

Ce que le hook streams rend coopératif, c'est une lecture bloquante sur un flux socket PHP et une attente dans stream_select(). Avant de compter dessus, vérifiez que votre client attend de l'une de ces deux manières, ou de l'une de celles qu'ajoute la catégorie net (ci-dessous). Plusieurs clients courants ne le font pas, et pour eux rien ne change :

  • ext/curl. curl_exec() et curl_multi_* parlent aux sockets eux-mêmes, sous les flux PHP, si bien que le hook ne les voit jamais. Cela couvre tous les clients HTTP bâtis sur curl — Guzzle avec son handler par défaut en fait partie. Un client curl peut être pointé vers le handler à wrappers de flux, qui, lui, passe par les flux PHP.

  • socket_select(). C'est ext/sockets, une API différente sur des descripteurs bruts, et elle n'est pas interceptée. stream_select() l'est. Il en va de même pour l'attente dans stream_socket_accept(), qui n'est pas interceptée non plus.

  • Les flux unix://, udp:// et udg://, les paires de sockets, les flux issus de socket_export_stream() sur des sockets qui ne sont pas TCP, et ssl:///tls:// une fois le chiffrement actif. Leurs tables d'opérations sont privées à PHP ou à OpenSSL, si bien que le hook streams ne les atteint pas. La catégorie net, si : quand elle est listée, une lecture de flux (fread(), fgets() et consorts) sur l'un d'eux suspend la Fiber comme sur un flux tcp://. Sans net, ils détiennent le thread comme auparavant (avant que stream_socket_enable_crypto() ne réussisse, les lectures d'un flux SSL sont déléguées à la lecture du socket en clair et suspendent dans les deux cas). stream_socket_recvfrom() n'est pas une lecture de flux : il appelle recvfrom() directement, sans attente propre à PHP à suspendre, si bien que sur un socket udp:// ou udg:// il détient le thread jusqu'à l'arrivée d'un datagramme, même avec net. socket_export_stream() crée un flux du transport qu'utilise le socket : un socket UDP, unix ou unix en datagrammes devient un flux udp://, unix:// ou udg://, un socket TCP un flux tcp:// ordinaire (que streams seul couvre déjà), et un socket de tout autre type le flux générique qu'a une paire de sockets.

  • L'établissement de connexion et la résolution DNS. Avec net, un établissement de connexion via les transports de flux de PHP suspend la Fiber : stream_socket_client(), fsockopen() et les clients bâtis sur eux. ftp_connect() et socket_connect() se connectent par eux-mêmes, hors de la couche de flux, et ne sont pas couverts. La résolution DNS ne suspend pas non plus : un nom d'hôte est résolu sur le thread worker avant que l'établissement de connexion ne commence, si bien qu'un résolveur lent le détient. Un client doté de sa propre bibliothèque réseau (pdo_pgsql et pgsql via libpq, l'extension memcached via libmemcached) se connecte et attend à l'intérieur de cette bibliothèque, qu'aucune des deux catégories ne voit. Un client qui garde sa connexion ouverte en bénéficie à chaque requête quoi qu'il en soit.

    Un établissement de connexion persistante fait exception, et cela couvre plus que pfsockopen() et STREAM_CLIENT_PERSISTENT : PDO_MySQL avec PDO::ATTR_PERSISTENT, mysqli avec le préfixe d'hôte p: et phpredis avec pconnect() ouvrent eux aussi leurs flux comme persistants, et leurs établissements de connexion gardent le thread pendant l'attente, comme avant l'existence de net. La raison en est la liste des persistants. Un flux persistant y est inscrit sous sa clé avant d'être connecté, si bien qu'une seconde Fiber demandant la même clé pendant que la première attend trouverait un flux encore sans descripteur, le prendrait pour une connexion morte et le fermerait sous la Fiber encore en train de se connecter. Certains clients donnent à chaque connexion une clé qui lui est propre (c'est le cas de mysqlnd), et rien ne peut retrouver ces flux de cette façon, mais le serveur décide selon que le flux est persistant ou non, pas selon qui pourrait le retrouver : ils détiennent donc eux aussi le thread. Le coût porte sur l'établissement de la connexion, qu'une connexion persistante ne paie qu'une fois, à sa première ouverture, et non sur les lectures et écritures qui s'y font.

  • MySQL atteint via localhost. Le client MySQL lit localhost comme « utiliser le socket unix », qui n'est pas un flux tcp://, si bien que le hook streams ne le voit pas. Écrivez plutôt 127.0.0.1 dans le DSN, ou listez net, qui couvre les flux unix://. Cela vaut pour PDO_MySQL comme pour mysqli, et c'est facile à manquer parce que tout continue de fonctionner, simplement sans le bénéfice.

  • Les écritures. Avec streams seul, seules les lectures attendent. La disponibilité en lecture reste acquise tant qu'une Fiber possède le descripteur et que rien d'autre ne le vide, alors que la place dans le tampon d'envoi est accordée et retirée par le pair : une Fiber réveillée sur la disponibilité en écriture peut trouver la fenêtre refermée au moment où l'écriture s'exécute, après quoi PHP bloque de toute façon le thread pour son délai complet. Une écriture qui remplit le tampon du socket se comporte donc exactement comme sans le hook. En pratique cela coûte peu : c'est attendre une réponse qui prend du temps, pas remettre une requête au noyau. (Les écritures sont tout de même examinées, pour une seule chose : savoir quel échange de quelle Fiber la connexion transporte en ce moment — voir la note sur les connexions partagées plus bas. Sur une connexion qu'aucune autre Fiber n'utilise, c'est-à-dire toutes les connexions dans le cas ordinaire, l'écriture suit le chemin natif intact.)

    Avec net listé, une écriture qui doit attendre de la place suspend elle aussi la Fiber, et le descripteur est revérifié après chaque réveil, si bien qu'une fenêtre refermée entre-temps coûte une suspension de plus plutôt que le thread. Un flux en clair dont le délai est infini (default_socket_timeout=-1) fait exception : PHP y envoie avec un send() bloquant et n'attend jamais de lui-même, il n'y a donc rien à suspendre et l'écriture détient le thread comme sans le hook. Un flux TLS au délai infini est différent : la propre boucle d'OpenSSL attend dans poll() sans limite, et cette attente est suspendue.

Dans ce périmètre, le hook conserve le contrat natif : les délais de socket (stream_set_timeout(), default_socket_timeout) s'appliquent inchangés, une lecture expirée rapporte toujours timed_out via stream_get_meta_data(), et l'identité du flux est intacte, si bien que socket_import_stream() et consorts continuent de fonctionner. Une limite sur le délai : l'échéance n'est examinée qu'une fois par tick de l'ordonnanceur, elle ne se déclenche donc pas avant le tick suivant — au mieux 100 µs en mode worker et 50 µs dans le pool asynchrone, et davantage sous charge, puisqu'un tick dure aussi longtemps que ce que le worker exécute. Le pool asynchrone fait des pauses plus longues que cela quand il ne se passe rien, mais jamais au-delà d'une échéance qu'il détient : une échéance de lecture ou d'écriture raccourcit la pause pour tomber sur elle-même, si bien qu'attendre plus longtemps ne retarde pas le délai. Avec default_socket_timeout à 60 secondes, ce n'est pas quelque chose que la plupart des déploiements peuvent observer.

stream_select() est intercepté en attendant sur les descripteurs que ses trois tableaux nomment, puis en remettant l'appel à PHP avec le délai mis à zéro, si bien que PHP décide encore de tout ce que vous pouvez observer : le nombre renvoyé, la réécriture des tableaux réduits aux flux prêts, les avertissements et les erreurs d'argument. L'attente est sautée — et l'appel s'exécute exactement comme sans le hook — dès que les tableaux contiennent quelque chose que le hook ne remplacera pas : un flux en lecture avec des données déjà en tampon (auquel stream_select() répond depuis le tampon, sans regarder un descripteur), un flux sans descripteur du tout, un élément qui n'est pas un flux vivant (un flux fermé laissé dans le tableau, par exemple — PHP y répond par une erreur, pas par une attente), un descripteur dont le noyau ne surveillera jamais la disponibilité (un fichier ordinaire est le cas usuel, et il compte comme prêt dès qu'on le demande), ou un descripteur égal ou supérieur à FD_SETSIZE, que le select() de PHP refuse d'emblée. Ce dernier cas est un vrai plafond plutôt qu'une formalité : un worker occupé peut détenir plus de 1024 descripteurs ouverts, et un stream_select() qui en nomme un échoue de la même façon avec ou sans le hook.

Connexions partagées entre Fibers concurrentes

Une connexion partagée entre Fibers concurrentes est sûre pour les clients qu'OxPHP protège, et n'y gagne rien. C'est la forme normale d'une application en mode worker plutôt qu'un cas limite : WordPress, Laravel et Symfony ouvrent leurs clients de base de données et de cache une fois à l'amorçage du worker et confient les mêmes à chaque requête, et à moins de réécrire leur couche d'accès aux données, on ne peut pas leur demander une connexion par Fiber.

Un protocole client est une suite d'échanges — écrire une commande, lire la réponse — sans que rien sur la connexion ne marque où l'un se termine. Une Fiber stationnée sur une lecture est stationnée au milieu d'un échange, et la commande d'une seconde Fiber qui y atterrit casse le protocole. Les deux clients échouent différemment sur ce point, et aucun ne le fait bien : mysqlnd suit l'état de sa connexion et refuse la commande avant d'envoyer quoi que ce soit, tandis que phpredis n'a pas un tel contrôle et les deux Fibers lisent les réponses l'une de l'autre — les données d'une requête renvoyées à une autre sans qu'aucune erreur soit levée.

Une Fiber revendique donc une connexion avant de s'en servir, aux deux niveaux où cela doit se faire : les opérations de socket, qui gardent les octets en ordre, et les points d'entrée client de PDO et mysqli, puisque le refus de mysqlnd survient avant toute E/S et que rien au niveau du socket ne peut être atteint à temps pour l'empêcher. Une autre Fiber qui atteint la même connexion attend qu'elle soit rendue au niveau client, où rien d'autre que l'identité de la connexion n'est détenu ; au niveau du socket, elle n'attend pas mais est refusée, comme l'est un délai de socket, parce qu'une Fiber suspendue dans une opération sur le flux de quelqu'un d'autre détiendrait un pointeur que son propriétaire peut libérer. phpredis est protégé au niveau client aussi, méthode par méthode, exactement pour cette raison. Ce que le niveau client revendique est la connexion elle-même et non l'objet PHP qui la détient, si bien qu'une connexion persistante atteinte via plusieurs objets PDO compte pour une seule, et qu'une connexion ouverte avec PDO::connect() — qui renvoie la sous-classe propre au pilote plutôt qu'un PDO — est couverte comme n'importe quelle autre.

Lisez donc le gain du hook comme appartenant aux connexions qu'une Fiber ouvre pour elle-même : une tâche asynchrone faisant ses propres appels HTTP ou base de données, une requête ouvrant son propre client. Ce qu'une connexion partagée obtient, c'est le thread worker rendu pendant qu'elle attend, pour que d'autres requêtes exécutent le travail qui n'est pas sur cette connexion ; ses propres échanges s'exécutent l'un après l'autre, exactement comme avec le hook désactivé. Quatre limites méritent d'être connues :

  1. Une Fiber qui a interrogé une fois garde la connexion jusqu'à la fin de sa requête, parce que la fin de la requête est le premier moment certainement postérieur à la fin d'un échange. Elle la garde aussi pendant qu'elle est stationnée sur autre chose, si bien qu'une requête qui a interrogé puis attend un travail à elle qui a besoin de la même connexion s'attend elle-même ; les deux se séparent sur la borne ci-dessous au lieu d'avancer.

  2. L'attente est toujours bornée : par le plus petit de max_execution_time et default_socket_timeout. Ne définissez ni l'un ni l'autre et cette borne est de 30 secondes, puisqu'une SAPI serveur prend les valeurs par défaut du moteur : 30 pour le premier et 60 pour le second ; elle vaut les 60 de default_socket_timeout uniquement là où max_execution_time est à 0. max_execution_time est lu tel que la requête l'a en ce moment, puisque set_time_limit() est la manière dont une requête déclare combien de temps elle peut s'exécuter ; default_socket_timeout est lu tel qu'au démarrage du processus, parce que c'est l'échéance par défaut d'une opération de socket, et une requête qui le resserre pour un appel à elle (chose courante pour une bibliothèque, et qu'elle laisse souvent derrière elle) ne doit pas raccourcir cette borne pour les requêtes qui la suivent sur le même worker. Passé la borne, l'appel retombe sur le comportement non protégé et en donne la raison dans le log du serveur : pour PDO et mysqli, il est remis au client, dont le propre refus d'une commande émise au milieu d'un échange est l'erreur que l'application gère déjà, tandis que pour phpredis, qui n'a pas un tel refus et lirait à la place la réponse de quelqu'un d'autre, il lève une RedisException et n'envoie rien.

    Un appel PDO fait exception, ici comme pour le code qui s'exécute hors de toute Fiber (ci-dessous) : PDO::rollBack() sur une connexion persistante qu'une autre Fiber détient, s'il y trouve une transaction ouverte, n'envoie rien et renvoie false, parce que cette transaction est celle du détenteur et qu'un ROLLBACK envoyé entre ses instructions y mettrait fin. L'erreur qu'il signale, de la manière que dicte le mode d'erreur, est une erreur que la connexion a déjà enregistrée, qui peut être celle du détenteur. Le démontage d'objets propre à PDO est lui aussi réservé au détenteur. Tant qu'une requête ou une tâche détient une connexion persistante, abandonner un handle sur celle-ci n'annule la transaction qui y est ouverte et n'exécute le nettoyage de fin de requête du pilote que si le détenteur a été le dernier à utiliser ce handle (il l'a construit, ou a depuis appelé à travers lui l'une des méthodes PDO qu'OxPHP protège), où que le handle soit détruit, y compris par le collecteur de cycles de PHP s'exécutant dans une autre requête. Tout autre handle saute les deux. Sur une connexion que personne ne détient, chaque handle effectue les deux, comme auparavant.

    Un conflit au niveau du socket n'a pas de borne propre parce qu'il n'attend jamais : l'opération échoue aussitôt comme le fait un délai expiré, si bien que stream_get_meta_data() rapporte timed_out. Cela peut coûter plus que l'appel : un client est libre d'interpréter une opération refusée comme quelque chose de plus grave qu'une commande échouée, et mysqlnd le fait des deux côtés d'un échange. Une écriture qui n'envoie rien marque la connexion comme perdue et la ferme, une lecture qui revient vide la marque comme perdue sur place, et chaque commande ultérieure sur elle reçoit une réponse tirée de cet état sans toucher au réseau. La perte porte sur la connexion entière plutôt que sur l'appel isolé, aussi longtemps que quelque chose détient le handle. C'est la revendication au niveau client décrite plus haut qui empêche les commandes d'un client protégé d'atteindre ce refus ; un protocole écrit à la main sur un socket brut doit traiter une opération refusée, de l'un ou l'autre type, comme il traite une connexion fermée. Deux Fibers détenant chacune ce que l'autre attend se séparent donc sur cette borne au lieu de s'attendre l'une l'autre pour de bon.

    Le code qui s'exécute hors de toute Fiber ne peut pas du tout attendre une revendication, et seul le niveau socket s'applique à lui. En mode worker avec RUNTIME_HOOKS=streams, il s'agit surtout d'un destructeur que la boucle du worker exécute entre les requêtes quand elle collecte les cycles, pendant que d'autres requêtes sont stationnées. Une opération de socket qu'il effectue et qui devrait attendre, sur une connexion dont le détenteur y est stationné en attente de sa réponse, échoue aussitôt comme le fait un délai expiré et n'envoie rien ; à tout autre moment, elle se poursuit. Le niveau client n'y est pas appliqué, parce que sa revendication dure jusqu'à la fin de la requête du détenteur : il refuserait à un tel code une connexion que le détenteur n'utilise pas à ce moment-là, ce qui est le cas la plupart du temps. Ce que coûte un refus dépend du client, comme ci-dessus. phpredis fait échouer l'appel et le détenteur garde sa réponse. mysqlnd soit refuse la commande d'après son propre état, soit, lorsque l'échange du détenteur a laissé cet état prêt, voit son écriture refusée et ferme la connexion, et la requête du détenteur se termine comme décrit au point 4. Mieux vaut donc ne pas utiliser de client partagé du tout dans un destructeur.

  3. Certains cas sont délibérément laissés sans couverture. Un objet statement ou résultat conservé d'une requête à l'autre, qu'aucun appel revendiqué ne précède : PDOStatement::execute() sur un statement préparé dans une requête antérieure se comporte comme sans revendication du tout, et cela coûte plus que l'appel : atteignant le socket pendant qu'une Fiber quelconque est stationnée sur la connexion, il y est refusé, et pour mysqlnd un refus fait perdre la connexion, comme ci-dessus, aussi bien sur la lecture qui récupère la ligne suivante d'un résultat non mis en tampon que sur l'écriture qui envoie la commande. Un protocole écrit à la main sur un socket brut (écrire une commande, se suspendre sur autre chose, lire la réponse plus tard), puisque le niveau socket ne refuse que pendant que le détenteur est stationné sur la réponse elle-même, et qu'entre ces deux points une autre Fiber s'empare de la connexion ; les trois clients ci-dessus sont couverts ici par leur revendication au niveau client, dont un protocole écrit à la main n'a pas d'équivalent.

    Les connexions persistantes sont couvertes ; voir Connexions PDO persistantes ci-dessous.

  4. Fermer une connexion partagée pendant qu'une autre requête est stationnée à la lire met fin à cette requête, avec un 500 et une ligne de log nommant ce qui s'est passé. La revendication tient deux échanges à l'écart l'un de l'autre ; elle ne peut pas garder une connexion en vie, et la réponse de la requête stationnée se trouve sur une connexion qui n'existe plus — la réponse honnête est donc d'y mettre fin plutôt que de rendre ce que la mémoire libérée contient désormais. Les appels capables de cela dépendent du client, et un seul d'entre eux attend : mysqli::close() et mysqli_close() sont des appels revendiqués, ils attendent donc le détenteur et ferment une fois cette attente épuisée, tandis que Redis::close() est revendiqué aussi mais lève une RedisException au lieu de fermer. Tout le reste ferme aussitôt — fclose() sur un flux brut, et, surtout, PDO, qui n'a pas de méthode close() du tout : une connexion est libérée en abandonnant la dernière référence au handle (unset($pdo), le réassigner, ou le laisser sortir de portée), et ce chemin s'exécute dans le démontage d'objets du moteur lui-même, où aucune revendication n'est consultée avant que la connexion ne soit fermée. L'assistant de reconnexion qui réassigne un handle PDO partagé met donc fin aux requêtes stationnées sur l'ancienne connexion, sans l'attente bornée qu'obtient son équivalent mysqli. La requête est prévenue dès que la fermeture a lieu plutôt qu'à sa propre échéance de lecture, qui pour mysqlnd serait mysqlnd.net_read_timeout — un jour entier par défaut. Les applications qui ferment une connexion partagée pour forcer une reconnexion (le $wpdb->check_connection() de WordPress est le cas usuel) doivent s'attendre à ce que les requêtes qui y sont stationnées à ce moment-là échouent plutôt qu'elles ne renvoient de mauvaises données.

Une dernière limite : faire tourner le hook streams sous un ordonnanceur de Fibers en espace utilisateur (AMPHP, Revolt) retombe sur des E/S bloquantes. Une Fiber démarrée par un tel ordonnanceur s'exécute sur son propre contexte, que l'ordonnanceur d'OxPHP ne peut pas reprendre, si bien que le hook le détecte et prend le chemin natif plutôt que de corrompre l'un ou l'autre ordonnanceur. Il s'agit là d'une Fiber qu'un ordonnanceur en espace utilisateur démarre à l'intérieur d'une requête. La Fiber propre de la requête est pilotée par OxPHP, et en mode worker c'est une vraie Fiber (Fiber::getCurrent() dans une requête la renvoie), si bien que les bibliothèques qui n'ont besoin que de distinguer les requêtes concurrentes fonctionnent sans aucun repli. C'est le même fait qui explique pourquoi Revolt refuse d'exécuter sa boucle d'événements depuis l'intérieur d'une requête en mode worker.

Connexions PDO persistantes

PDO::ATTR_PERSISTENT garde la connexion du pilote dans un pool que tout le worker partage, et PDO atteint ce pool en deux étapes qui ne supportent pas d'être entrelacées : il cherche une connexion dans le pool avant de se connecter, et enregistre ensuite ce qu'il a construit. Deux constructeurs qui se chevauchent manquent tous deux le pool, et le second enregistrement remplace la première entrée, fermant cette connexion sous la requête qui y lit sa réponse, quelle qu'elle soit. Le chevauchement est le cas ordinaire, puisque static $pdo ??= new PDO(...) est atteint par chaque requête qui arrive avant la fin de la première connexion.

Les constructeurs qui demandent une connexion persistante vers une même source de données s'exécutent donc un par un par worker, et le second trouve ce que le premier a enregistré. La source de données est celle que PDO résout (un alias issu de pdo.dsn.* dans php.ini compte comme le DSN qu'il nomme), et les constructeurs vers des sources différentes ne s'attendent pas, en règle générale, les uns les autres : un constructeur bloqué à se connecter à un serveur qui accepte la connexion et ne répond jamais ne retient donc pas les constructeurs vers un serveur sain. Deux cas attendent davantage plutôt que moins :

  • Les DSN identiques jusqu'à un deux-points. Un DSN n'est comparé que jusqu'au premier deux-points qui suit son préfixe de pilote (un hôte IPv6 est une façon d'en avoir un), parce que PDO joint DSN, utilisateur et mot de passe dans sa clé de pool avec des deux-points et n'échappe rien : un DSN prolongé par :… peut donc nommer la même entrée de pool que celui qui ne l'est pas. Les sources identiques jusqu'à ce deux-points partagent une même attente, y compris une source bloquée : deux hôtes IPv6 dans le même premier groupe de chiffres s'attendent l'un l'autre comme le faisaient autrefois tous les constructeurs d'un worker.
  • Les DSN uri: et les DSN qui ne sont pas des chaînes. Ils ne peuvent pas être résolus sans refaire ce que PDO en fait, si bien qu'un tel constructeur attend qu'aucun constructeur persistant vers quelque source de données que ce soit ne s'exécute sur son worker, et tous attendent pendant qu'il s'exécute. Il ne détient rien pendant qu'il attend, il ne retient donc pas les autres, mais tant qu'un constructeur vers un serveur qui ne répond jamais s'exécute, il ne peut pas trouver ce moment, et quand son attente est épuisée il poursuit sans sérialisation. Le cas du serveur bloqué ci-dessus atteint donc toujours les DSN uri: et objets, et un tel DSN qui nomme le serveur bloqué lui-même retient tous les constructeurs persistants de son worker aussi longtemps que dure son établissement de connexion, exactement comme auparavant.

Cette attente est bornée comme celles ci-dessus, mais ce qu'elle coûte une fois épuisée n'est pas une erreur que le client lève : un constructeur dont le budget s'épuise poursuit sans sérialisation et le signale dans le journal du serveur, et deux constructeurs qui se chevauchent ainsi remplacent l'entrée de pool l'un de l'autre comme auparavant. Cela met fin à la requête qui lit sa réponse sur la connexion remplacée et, parce qu'un handle du pool est libéré quel que soit le nombre d'objets PDO qui pointent encore vers lui, peut mettre fin au processus worker lorsqu'un de ces objets est détruit plus tard. À l'intérieur de la propre Fiber d'un ordonnanceur en espace utilisateur, ce budget est épuisé aussitôt : un tel contexte ne peut pas être suspendu, si bien qu'un constructeur qui en trouve un autre en cours abandonne immédiatement.

L'autre étape est la vérification de vivacité que PDO effectue avant de remettre une connexion du pool : un ping sur la connexion elle-même. Envoyé pendant que le détenteur est stationné en attente de sa réponse, il serait refusé, PDO considérerait la connexion comme morte et en ouvrirait une seconde, et ce que l'application voulait partager ne le serait plus. Une connexion qu'une autre Fiber détient est donc déclarée vivante sans le ping, mais seulement pour un constructeur qui la laisserait exactement en l'état (là où un pilote ne fournit aucune vérification de vivacité, comme pdo_sqlite, la même réponse remplace celle de PDO). PDO écrit le mode d'erreur, l'indicateur d'autocommit et le reste de ses options sur un handle du pool qu'il remet, et sur une connexion en cours d'utilisation ces écritures atterriraient chez le détenteur. La connexion n'est donc partagée qu'avec un constructeur qui n'y écrirait rien de nouveau :

  • chaque option qu'il passe est une option que PDO stocke sur le handle lui-même (PDO::ATTR_ERRMODE, PDO::ATTR_CASE, PDO::ATTR_ORACLE_NULLS, PDO::ATTR_DEFAULT_FETCH_MODE ; l'entrée PDO::ATTR_PERSISTENT qui nomme le pool ne compte pas), et
  • chacune porte la valeur que la connexion a déjà.

Une option dont le pilote doit être informé est refusée quelle que soit sa valeur, PDO::ATTR_AUTOCOMMIT et PDO::ATTR_STRINGIFY_FETCHES compris, sur tous les pilotes, tiers inclus. Un constructeur qui ne remplit pas l'une ou l'autre condition apprend que la connexion est morte et en obtient une à lui, et le journal du serveur le signale au plus une fois par seconde. Pour une clé de pool dont les options portent PDO::ATTR_EMULATE_PREPARES ou PDO::ATTR_STRINGIFY_FETCHES, comme celles de Laravel, cela signifie : partagée entre requêtes, et entre Fibers qui construisent avant d'interroger, mais un constructeur qui arrive pendant qu'une autre requête détient la connexion ouvre la sienne. Une revendication court de la première requête SQL du détenteur jusqu'à la fin de sa requête.

Une connexion que rien n'utilise est vérifiée comme auparavant, y compris celle dont le détenteur a abandonné son dernier handle, si bien qu'une connexion réellement morte est toujours remplacée. Cette vérification est une commande comme une autre et elle est revendiquée comme telle : une requête SQL arrivant par un client protégé pendant qu'elle est en cours l'attend, dans la borne habituelle. Les cas non couverts ci-dessus restent non couverts ici aussi.

Activation et coût

1, true et all activent toutes les catégories, streams et net comprises. Un déploiement qui définit déjà RUNTIME_HOOKS=1 pour les hooks sleep se met donc à intercepter les sockets à la mise à niveau sans la moindre modification de sa part ; listez les catégories explicitement (RUNTIME_HOOKS=sleep, ou RUNTIME_HOOKS=sleep,streams pour conserver ce qu'activait la 0.12.0) si ce n'est pas ce que vous voulez. Activer streams modifie aussi en mémoire, au démarrage, une entrée de la table d'opérations de flux de PHP ; la page est remise comme elle a été trouvée, mais sur une plateforme où sa protection d'origine ne peut pas être déterminée, elle reste inscriptible — une petite perte de durcissement, signalée dans le log du serveur. Activer net modifie le même type d'entrées dans les tables d'opérations des autres transports de socket et d'OpenSSL, aux mêmes conditions, et fait répondre la bibliothèque passerelle à poll() pour les appels que fait le code propre de PHP ; tout autre appelant, le serveur lui-même compris, est transmis directement à la libc.

Le coût, mesuré : environ 3–5 µs par aller-retour de socket sur un worker sans rien d'autre de stationné, environ 5–6 µs avec 64 Fibers stationnées sur des descripteurs, et environ 7–11 µs avec 200. La disponibilité est résolue via un ensemble que le noyau conserve entre les attentes, si bien que ce que le chiffre suit est le nombre de descripteurs devenus prêts, pas le nombre en attente. Un worker inactif attend sur ces descripteurs au lieu de dormir un intervalle fixe et de remarquer la disponibilité à son tick suivant, ce qui compte énormément : dormir à l'aveugle coûtait environ 2 ms par aller-retour à la place.

Un stream_select() large porte un surcoût que le cas étroit n'a pas, parce que chaque descripteur nommé par un appel est enregistré avant l'attente et retiré après. Mesuré en retirant l'attente elle-même — chaque descripteur déjà lisible, si bien que l'appel revient aussitôt et qu'il ne reste que le surcoût — cela donne environ 6 µs contre 5 µs sans hook à un descripteur, 56 µs contre 9 µs à 64, et 130–150 µs contre 16–21 µs à 200 : environ 0,65 µs par descripteur.

Lisez cela comme un coût fixe par appel, pas comme un ralentissement du même travail. Un stream_select() qui attend réellement — ce pour quoi une boucle de select est écrite — l'éclipse : une milliseconde d'attente ramène même le chiffre à 200 descripteurs à environ un dixième de l'appel, et ce qu'il achète est le thread worker, que l'appel non intercepté détient pendant toute l'attente. Deux formes font exception, et pour elles la catégorie est mieux laissée désactivée : un appel sur de nombreux descripteurs qui n'attend presque jamais, parce qu'il n'y a pas de temps de thread à récupérer ; et une requête qui est la boucle d'événements, où le thread n'a de toute façon rien d'autre à exécuter. Les clients qui attendent sur une seule connexion — mysqlnd, phpredis, les wrappers de flux HTTP — se situent en haut du tableau, où le surcoût est de l'ordre de la microseconde.

RUNTIME_HOOKS et oxphp_sleep()

Le hook ne remplace pas la primitive maison — ils couvrent des cas différents :

  • Choisissez oxphp_sleep() / oxphp_usleep() dans le code que vous écrivez. Ils suspendent la Fiber en mode worker sans indicateur d'environnement à définir, et oxphp_sleep() accepte des secondes fractionnaires (oxphp_sleep(0.25)), une précision que le sleep() natif (secondes entières) ne peut pas exprimer. Ils ne sont pas exemptés des endroits où une Fiber ne peut pas suspendre : là, ils détiennent le thread worker exactement comme les fonctions natives interceptées.
  • Activez RUNTIME_HOOKS=sleep pour le code que vous ne pouvez pas modifier, comme un framework ou une bibliothèque tierce qui appelle directement les sleep()/usleep() natifs. Il est désactivé par défaut, ne fait rien hors d'une Fiber et conserve le contrat de retour natif : un sleep() intercepté prend toujours un int, renvoie 0 quand il suspend et ce que renvoie la fonction native quand il se replie sur elle. La vérification de l'argument est la sienne, comme indiqué plus haut.

Compter sur RUNTIME_HOOKS pour vos propres gestionnaires attache leur coopérativité à un réglage de déploiement plutôt qu'au code ; préférez-y le oxphp_sleep() explicite.

État partagé

Primitives de concurrence intra-processus (OxPHP\Shared\Counter, Map, Channel, Mutex, Once, Pool, Atomic, Flag, Registry). Voir État partagé pour un tour d'horizon de l'API.

Variable Valeur par défaut Description
SHARED_ENABLED true Booléen — voir Valeurs booléennes. Interrupteur principal de l'ensemble du sous-système OxPHP\Shared\*. Défini à false, aucune de ses classes n'est enregistrée : new OxPHP\Shared\Counter() lève Error: Class "OxPHP\Shared\Counter" not found, et /__ox_shared/* ainsi que les métriques oxphp_shared_* sont absents. Protégez avec class_exists('OxPHP\Shared\Counter') le code qui doit fonctionner dans les deux cas. Ne protégez pas avec interface_exists('OxPHP\Shared\Shareable') : cela reste vrai, car l'extension enregistre cette interface dans tous les cas
SHARED_MAX_ENTRIES 100000 Plafond global sur le nombre d'objets Shared vivants (chaque Counter, Map, Channel, … compte pour un). Créer un objet au-delà échoue avec CapacityException. Les clés à l'intérieur d'une Map n'y comptent pas ; bornez une Map avec maxEntries
SHARED_MAX_BYTES 1073741824 (1 GiB) Plafond global sur la mémoire estimée pour l'ensemble des objets Shared, vérifié uniquement à la création d'un objet (CapacityException). Les objets qui existent déjà peuvent continuer à grossir au-delà, si bien que le total peut dépasser le plafond
SHARED_SOFT_LIMIT_RATIO — Déprécié et ignoré ; le définir journalise un WARN au démarrage. Il n'a jamais eu d'effet : rien n'est délesté à l'approche de SHARED_MAX_BYTES / SHARED_MAX_ENTRIES, qui ne font que refuser la création d'un nouvel objet Shared une fois atteints. Pour être prévenu à l'avance, alertez sur oxphp_shared_capacity_saturation, publié tant que SHARED_METRICS_ENABLED est actif (voir Observabilité de Shared*)
SHARED_METRICS_ENABLED true Booléen. Active/désactive l'exposition Prometheus oxphp_shared_*
SHARED_INTROSPECTION_ENABLED true Booléen. Active/désactive l'API d'introspection /__ox_shared/* sur le serveur interne
SHARED_INTROSPECTION_PREVIEW_ENABLED true Booléen. Active/désactive les aperçus de valeurs dans les réponses d'introspection (désactivez-les lorsque les aperçus pourraient divulguer des données sensibles)
SHARED_CYCLE_DETECT_DEPTH 16 Profondeur du parcours BFS lors de la vérification des cycles. À augmenter pour des graphes profonds légitimes
SHARED_CYCLE_DETECT_EDGES 10000 Nombre d'arêtes parcourues lors de la vérification des cycles. À augmenter pour des graphes denses légitimes
SHARED_MAX_VALUE_SIZE 1048576 (1 MiB) Plafond de taille par valeur. L'insertion d'une valeur plus grande échoue immédiatement
SHARED_MAX_CHANNEL_BYTES 67108864 (64 MiB) Plafond de charge utile totale par canal
SHARED_POISON_STRICT — Déprécié et ignoré ; le définir journalise un WARN au démarrage. Il n'a jamais eu d'effet, et il n'existe aucune politique globale d'empoisonnement qu'il puisse régler : une exception levée à l'intérieur d'une closure Shared\Mutex n'empoisonne jamais le mutex, et le fait qu'une factory Shared\Once en échec désactive définitivement la cellule se choisit par instance ; passez Once\FailureMode::Poison au constructeur (voir Shared\Once)
SHARED_LOCK_DIAGNOSTICS warn (strict dans un build de débogage du binaire) Détecteur d'interblocages inter-threads pour Shared\Mutex : off, warn (journalise et compte les cycles) ou strict (les rompt en plus avec DeadlockException). En minuscules uniquement ; toute autre valeur donne silencieusement la valeur par défaut. Voir Détecteur d'interblocage
SHARED_LOCK_POLL_INTERVAL_MS 100 Fréquence d'analyse du détecteur d'interblocages, en millisecondes. Inutilisé quand SHARED_LOCK_DIAGNOSTICS=off
SHARED_PREVIEW_STRING_LIMIT 256 Troncature par chaîne dans les aperçus /__ox_shared/preview, en octets (coupée à une frontière de caractère)
SHARED_PREVIEW_ARRAY_LIMIT 20 Nombre d'entrées échantillonnées dans les aperçus /entry?id=…

Profilage

Profileur par échantillonnage qui émet des traces xhprof / speedscope. Voir Profilage pour les formats de sortie et l'intégration avec les visualiseurs.

Variable Valeur par défaut Description
PROFILER_ENABLED false Booléen — voir Valeurs booléennes. Interrupteur principal. Toutes les autres variables PROFILER_* sont tout de même analysées au démarrage, de sorte que les fautes de frappe apparaissent immédiatement
PROFILER_SAMPLE_RATE 0.0 Probabilité (0.0–1.0) qu'une requête soit échantillonnée. Les valeurs hors plage sont ramenées dans les bornes
PROFILER_INTERNAL false Booléen. Lorsque la valeur est vraie, les requêtes vers le serveur interne (/health, /metrics, endpoints de plugins) sont elles aussi éligibles à l'échantillonnage
PROFILER_AUTH_TOKEN (non défini) Secret que doivent porter les déclencheurs explicites (l'en-tête X-OxPHP-Profile, le cookie OXPROF, le paramètre de requête __oxprof), et jeton bearer des routes /__profiler/* du serveur interne. Non défini ou vide = aucun jeton requis : toute valeur de déclencheur non vide passe, et les routes /__profiler/* répondent sans bearer. L'échantillonnage PROFILER_SAMPLE_RATE et les fonctions PHP OxPHP\Profile\* ne le consultent jamais : avec PROFILER_ENABLED=true, un script qui appelle OxPHP\Profile\start() profile sa propre requête sans qu'aucun jeton n'intervienne
PROFILER_MAX_SPANS 50000 Plafond de spans de profil par requête. Les profils dépassant ce plafond sont tronqués
PROFILER_MAX_DEPTH 0 (pas de plafond) Profondeur d'imbrication maximale des appels enregistrés ; un appel plus profond et, sauf en cas d'auto-récursion au-delà de 32 frames, tout ce qu'il appelle sont absents du profil. Leur temps, lui, ne l'est pas : il apparaît comme temps exclusif de l'appel situé à la limite. Les appels écartés comptent tout de même dans PROFILER_MAX_SPANS. Ramené à 32, la profondeur que suit l'observateur du moteur ; avec PROFILER_ENABLED=true, une valeur plus grande est signalée comme ramenée au démarrage
PROFILER_OUTPUT_DIR /tmp/oxphp-profiles Répertoire des fichiers de profil sur disque
PROFILER_OUTPUT_FORMATS xhprof,speedscope Liste, séparée par des virgules, des formats de sortie à écrire sur disque
PROFILER_DISK_MAX_PER_SEC 10 Limite de débit sur les fichiers de profil écrits sur disque par seconde
PROFILER_RETENTION_COUNT 100 Nombre maximal de fichiers de profil conservés dans PROFILER_OUTPUT_DIR. Les fichiers plus anciens sont supprimés
PROFILER_EXPORT_URL (non défini) Endpoint distant vers lequel envoyer (POST) les profils. Lorsqu'il est défini, les écritures sur disque ont toujours lieu, sauf si PROFILER_OUTPUT_FORMATS est vide
PROFILER_EXPORT_FORMAT xhprof Format de transmission (wire) pour les envois vers PROFILER_EXPORT_URL
PROFILER_EXPORT_AUTH_TOKEN (non défini) Jeton bearer facultatif envoyé avec chaque requête d'export
PROFILER_EXPORT_XHGUI (détection automatique) Booléen. Force l'encapsulation de la charge utile d'export au format compatible XHGui. Non défini = détection automatique lorsque le chemin de PROFILER_EXPORT_URL se termine par /run/import (les indices d'hôte/de requête ne sont pas pris en compte)
PROFILER_EXPORT_BUGGREGATOR (détection automatique) Booléen. Force l'enveloppe Buggregator. Non défini = détection automatique lorsque le chemin de PROFILER_EXPORT_URL se termine par /api/profiler/store. L'enveloppe émet toujours du xhprof, si bien que PROFILER_EXPORT_FORMAT est ignoré dans ce cas (une valeur non-xhprof émet un avertissement, non fatal). Mutuellement exclusif avec PROFILER_EXPORT_XHGUI — activer les deux est une erreur de démarrage
PROFILER_EXPORT_APP_NAME (non défini) app_name Buggregator pour le regroupement par projet
PROFILER_EXPORT_TAGS (non défini) tags Buggregator sous la forme key=value,key2=value2 ; un jeton mal formé, une clé vide ou une clé en double est une erreur de démarrage

Exemples de configurations

Développement

bash
LISTEN_ADDR=127.0.0.1:8080 DOCUMENT_ROOT=./public LOG_LEVEL=debug ACCESS_LOG=all PHP_WORKERS=1 INTERNAL_ADDR=127.0.0.1:9090

Production (Framework)

bash
LISTEN_ADDR=0.0.0.0:80 DOCUMENT_ROOT=/var/www/html/public ENTRY_FILE=index.php PHP_WORKERS=8 QUEUE_CAPACITY=1024 LOG_LEVEL=warn ACCESS_LOG=error MAX_CONNECTIONS=10000 INTERNAL_ADDR=127.0.0.1:9090 RATE_LIMIT=100 RATE_WINDOW_SECONDS=60 TRUSTED_PROXIES=private HEADER_TIMEOUT_SECONDS=5 DRAIN_TIMEOUT_SECONDS=25 COMPRESSION_BROTLI_LEVEL=5 COMPRESSION_ZSTD_LEVEL=6 COMPRESSION_GZIP_LEVEL=6 STATIC_MAX_AGE=30d

Production (mode worker)

bash
LISTEN_ADDR=0.0.0.0:80 DOCUMENT_ROOT=/var/www/html/public WORKER_MODE_ENABLED=true ENTRY_FILE=../worker.php PHP_WORKERS=8 WORKER_MAX_MEMORY_MIB=128 QUEUE_CAPACITY=1024 LOG_LEVEL=warn ACCESS_LOG=error INTERNAL_ADDR=127.0.0.1:9090

TLS

bash
LISTEN_ADDR=0.0.0.0:443 TLS_CERT=/etc/ssl/oxphp/cert.pem TLS_KEY=/etc/ssl/oxphp/key.pem DOCUMENT_ROOT=/var/www/html/public ENTRY_FILE=index.php

Inspecter la configuration active

Lorsque le serveur interne est en cours d'exécution, interrogez l'endpoint /config pour voir la configuration résolue :

bash
curl -s http://localhost:9090/config | jq .
json
{ "access_log": "all", "async_in_flight_cap": 0, "async_max_fibers": 256, "async_queue_capacity": 0, "async_workers": 0, "brotli_level": 5, "deny_file": {"loaded": false}, "document_root": "/var/www/html/public", "drain_timeout_seconds": 25, "entry_file": "/var/www/html/public/index.php", "executor_type": "sapi", "gzip_level": 6, "header_timeout_seconds": 5, "listen_addr": "0.0.0.0:443", "log_level": "warn", "max_connections": 10000, "max_query_body": 524288, "php_workers": "8", "plugins": { "apm": { "db_capture_params": false, "enabled": true, "hooks_registered": 34, "slow_query_ms": 100 }, "otel": { "enabled": true, "protocol": "grpc", "service_name": "oxphp" } }, "queue_capacity": 1024, "queue_max_waiting": 1024, "queue_max_waiting_bytes": 67108864, "queue_wait_timeout_ms": 1000, "rate_limit": 100, "rate_window_seconds": 60, "runtime_hooks": [], "static_max_age": 2592000, "static_revalidate": false, "superglobals_enabled": true, "tls_enabled": true, "tls_min_version": "1.2", "tokio_workers": 4, "trace_context": true, "trusted_proxies": false, "worker_max_memory_mib": 0, "worker_mode_enabled": false, "zstd_level": 6 }
Note

La réponse /config servie expurge quelques clés que porte la représentation interne Config : les chemins du certificat et de la clé TLS ne sont jamais émis (tls_enabled indique si TLS est actif), et internal_addr et error_pages_dir sont retirés — topologie de déploiement et chemins de système de fichiers qui aident un attaquant et ne sont pas nécessaires aux collecteurs de métriques.

Prompts pour votre agent

Ces prompts supposent un agent connecté au serveur MCP de la documentation OxPHP — il lira lui-même les pages dont il a besoin.

prompt
Règle OxPHP pour mon projet. Lis la page `operations/configuration` via le serveur MCP de la documentation OxPHP. Regarde ce que fait tourner ce dépôt — le framework, le nombre de workers qu'il peut se permettre, la forme des requêtes qu'il sert — et donne-moi un bloc de variables d'environnement de production avec une justification pour chaque valeur que tu retiens.
prompt
Passe en revue ma configuration OxPHP. Lis la page `operations/configuration` via le serveur MCP de la documentation OxPHP. Compare les variables d'environnement que ce dépôt définit aux valeurs par défaut, et dis-moi lesquelles de mes surcharges ne servent à rien, lesquelles sont risquées, et quelles valeurs par défaut j'aurais dû surcharger sans le faire.

Voir aussi

Une erreur ? Signalez-la →