Shared\Once

OxPHP\Shared\Once exécute une closure d'initialisation exactement une fois pour une seule cellule Once et rend son résultat visible à tout appelant ultérieur de cette même cellule. C'est la primitive pour « la chose coûteuse qui ne doit se produire qu'au plus une fois ».

Pour obtenir de véritables sémantiques « exactement une fois dans l'ensemble du processus » inter-worker / inter-requête, liez le Once sous un nom via Shared\Registry::once(...) afin que chaque worker converge vers la même cellule. Le simple schéma du constructeur new Shared\Once() produit une cellule distincte par thread de worker en mode worker (l'amorçage de chaque worker exécute le constructeur) et par requête en mode traditionnel — ce qui donne une fois par cellule, et non une fois par processus.

Vue d'ensemble

  • Exécution unique entre workers pour une cellule. Deux workers en compétition dans getOrInit($factory) sur la même cellule Once n'exécutent la factory que sur l'un d'eux ; le perdant se bloque et reçoit la valeur du gagnant. Combinez avec Shared\Registry::once pour que « la même cellule » signifie « le même nom pour chaque worker ».
  • Une machine à quatre états. Une cellule est Uninitialized, Pending (une factory est en cours d'exécution), Ready ou Poisoned. Lisez-la avec status().
  • Pas de null ambigu. get() lève une exception sur une cellule non définie au lieu de renvoyer null, de sorte qu'un null stocké est une vraie valeur, pas une valeur « absente ».
  • Sûr en cas de réentrance. Appeler getOrInit() sur le même Once depuis l'intérieur de sa propre factory lève DeadlockException au lieu de rester bloqué.
  • Politique d'échec configurable. Par défaut, une factory en échec réinitialise la cellule pour qu'un appel ultérieur puisse réessayer. Activez Poison pour qu'une factory en échec désactive la cellule de façon permanente.
  • Partageable. Les instances vivent dans le registre et se propagent à travers les captures use et les entrées Shared\Map.

Référence de l'API

php
namespace OxPHP\Shared; final class Once implements Shareable { public function __construct(Once\FailureMode $onFactoryError = Once\FailureMode::Reset); public function get(): mixed; // throws if not Ready public function status(): Once\Status; // never throws public function trySet(mixed $value): bool; // true if this call won public function getOrInit(callable $factory): mixed; // runs factory at most once public function id(): int; } namespace OxPHP\Shared\Once; enum Status { case Uninitialized; case Pending; case Ready; case Poisoned; } enum FailureMode: int { case Reset = 0; case Poison = 1; }
Méthode Renvoie Cas d'usage
get valeur stockée Lire une valeur dont vous savez qu'elle est Ready. Lève une exception sur uninit / pending / poison.
status Once\Status Introspection / diagnostics. Ne lève jamais d'exception (l'observateur de poison sûr).
trySet gagnant ? Init en mode push pour une valeur déjà en main (ressource sans effet de bord).
getOrInit valeur stockée Init en mode pull ; la primitive canonique exempte de course critique.
id id de registre Corrélation pour la journalisation / l'observabilité.

Exemples

Configuration coûteuse chargée une fois par processus

php
<?php // Registry::once binds the cell under a name so every worker's bootstrap // converges on it. Without Registry the bare `new Once()` here would // create one cell PER worker thread, and the factory would run once // per worker, not once per process. $config = OxPHP\Shared\Registry::once( 'app-config', fn() => new OxPHP\Shared\Once(), ); oxphp_worker(function () use ($config) { $cfg = $config->getOrInit(function () { // Runs in exactly one worker process-wide; every other worker // (and every later request, in traditional mode) blocks here // and sees the result. return json_decode(file_get_contents('/etc/myapp.json'), true); }); echo $cfg['greeting']; });

getOrInit() est le schéma résistant à la ruée sur le cache (cache stampede) : sous une rafale de premiers accès concurrents, la factory s'exécute exactement une fois lorsqu'elle réussit, et chaque appelant — y compris ceux qui ont perdu la course — reçoit la valeur du gagnant. Si la factory gagnante lève une exception en mode Reset, l'appelant bloqué suivant devient l'initialiseur et réessaie, de sorte qu'une factory qui échoue de manière persistante sous charge réessaie en série plutôt que de se déployer en parallèle. Utilisez le mode Poison (ci-dessous) lorsqu'un échec doit au contraire être terminal.

Bifurquer selon l'état sans déclencher l'initialisation

php
<?php use OxPHP\Shared\Once\Status; $cfg = new OxPHP\Shared\Once(); $report = match ($cfg->status()) { Status::Ready => $cfg->get(), Status::Pending => 'initialising…', Status::Uninitialized => 'not started', Status::Poisoned => 'config load failed', };

status() sert à l'introspection — il ne déclenche jamais la factory et ne lève jamais d'exception, même sur une cellule empoisonnée. Pour réellement obtenir la valeur à l'abri des courses critiques, appelez getOrInit().

Initialisation par la valeur d'abord lorsque la valeur est déjà connue

php
<?php $buildSha = new OxPHP\Shared\Once(); // A plain value with no acquisition side effects — trySet is fine here. $buildSha->trySet(getenv('GIT_SHA') ?: 'unknown'); $sha = $buildSha->get(); // Ready after the trySet above

N'utilisez trySet() que pour des valeurs dont l'acquisition n'a pas d'effet de bord. Pour les ressources (connexions, descripteurs de fichiers, sockets), utilisez plutôt getOrInit() : un trySet() qui perd la course se contente d'abandonner une valeur simple au ramasse-miettes, tandis qu'une ressource acquise avant une course perdue fuirait.

Un retour false signifie que la cellule était déjà Ready ou Pending — cela ne garantit pas qu'un get() ultérieur réussira, car une factory Pending sur un autre thread peut encore échouer et réinitialiser la cellule (en mode Reset). N'écrivez pas if (!$o->trySet($v)) { $x = $o->get(); } ; si vous avez besoin de la valeur, appelez getOrInit().

Amorçage d'une connexion à la base de données

php
<?php // Name the cell so only one PDO connection is opened across the // entire OxPHP process. The factory acquires a resource — exactly // what `getOrInit`'s block-losers semantics protect. $pool = OxPHP\Shared\Registry::once('db-conn', fn() => new OxPHP\Shared\Once()); $conn = $pool->getOrInit(function () { return new PDO(getenv('DB_DSN'), getenv('DB_USER'), getenv('DB_PASS'), [ PDO::ATTR_PERSISTENT => true, ]); });

Pour un pool de connexions à plusieurs emplacements, voir Shared\PoolOnce vous donne une valeur ; Pool vous en donne N.

Échec rapide sur un prérequis défaillant

php
<?php use OxPHP\Shared\Once\FailureMode; // If this initialisation fails, the app cannot recover — poison the cell so // every later access fails loudly instead of retrying a doomed factory. $secrets = new OxPHP\Shared\Once(onFactoryError: FailureMode::Poison); $secrets->getOrInit(fn () => loadSecretsOrThrow());

Sémantique et pièges

  • get() lève une exception lorsque la cellule n'est pas Ready. UninitializedException pour une cellule vide ou Pending, PoisonedException pour une cellule empoisonnée. Utilisez status() pour bifurquer sans exception, ou getOrInit() pour obtenir la valeur en toute sécurité.
  • La factory s'exécute au plus une fois par initialisation réussie. Les appelants concurrents se bloquent sur le gagnant ; ils n'exécutent pas leur propre copie.
  • La politique d'échec est définie à la construction, pas par appel. Reset (par défaut) ramène la cellule à Uninitialized en cas d'échec de la factory pour qu'un appel ultérieur réessaie ; Poison rend la cellule définitivement Poisoned. Dans les deux modes, l'exception de la factory est relancée vers l'appelant courant.
  • Éventail complet de valeurs. Les scalaires, les tableaux et les valeurs Shareable imbriquées sont stockés puis relus. Les closures, les ressources et les objets PHP non Shareable lèvent TypeException.
La réentrance lève une exception

getOrInit() depuis l'intérieur de sa propre factory lève DeadlockException. Restructurez pour que l'appel interne utilise un Once différent.

Le poison est fidèle d'un thread à l'autre, pas identique en objet

Un objet exception PHP ne peut pas traverser les threads de worker ; une cellule empoisonnée capture donc la classe, le message et le code de l'échec. Les appelants ultérieurs, sur n'importe quel thread, reçoivent une nouvelle PoisonedException porteuse de ces informations — les mêmes détails, pas le même objet.

Exceptions

Exception Levée par
UninitializedException get() sur une cellule Uninitialized ou Pending.
PoisonedException get() / getOrInit() / trySet() sur une cellule Poisoned.
DeadlockException getOrInit() appelé récursivement sur le même Once depuis sa factory.
TypeException Une valeur stockée n'est pas sérialisable (closure, ressource).
StaleHandleException Toute méthode sur un handle dont l'entrée de registre a été évincée.

Si la factory elle-même lève une exception, celle-ci se propage inchangée jusqu'à l'appelant courant. En mode Reset, la cellule reste non initialisée et le prochain getOrInit réessaie ; en mode Poison, la cellule devient empoisonnée.

Observabilité

Voir Observabilité de l'état partagé. Références rapides :

  • GET /__ox_shared/entry?id=N expose { status: "uninitialized" | "pending" | "ready" | "poisoned", type: "Once" } ainsi qu'un aperçu de la valeur stockée lorsqu'elle est ready.

Quand ne pas l'utiliser

  • Des valeurs qui changent après leur création. Once s'écrit une seule fois. Utilisez Shared\Mutex ou Shared\Map lorsque l'état stocké est modifié.
  • Un état local à chaque worker. Les propriétés statiques de classe ou les variables globales de module sont moins coûteuses lorsque la valeur n'a pas besoin d'être partagée.
  • Des calculs coûteux par requête. Mettez en cache dans la requête, pas dans l'état partagé — sinon vous provoquerez une fuite de mémoire.

Voir aussi

  • État partagé — vue d'ensemble et modèle mental.
  • Shared\Mutex — quand la valeur à usage unique est ensuite modifiée.
  • Shared\Pool — init unique de N ressources équivalentes.
  • Shared\Map — init par clé via getOrSet($key, $factory).