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 celluleOncen'exécutent la factory que sur l'un d'eux ; le perdant se bloque et reçoit la valeur du gagnant. Combinez avecShared\Registry::oncepour 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),ReadyouPoisoned. Lisez-la avecstatus(). - Pas de null ambigu.
get()lève une exception sur une cellule non définie au lieu de renvoyernull, de sorte qu'unnullstocké 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èveDeadlockExceptionau 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
Poisonpour 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
useet les entréesShared\Map.
Référence de l'API
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
// 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
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
$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 aboveN'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
// 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\Pool — Once vous donne une valeur ; Pool vous en donne N.
Échec rapide sur un prérequis défaillant
<?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 pasReady.UninitializedExceptionpour une cellule vide ouPending,PoisonedExceptionpour une cellule empoisonnée. Utilisezstatus()pour bifurquer sans exception, ougetOrInit()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 àUninitializeden cas d'échec de la factory pour qu'un appel ultérieur réessaie ;Poisonrend la cellule définitivementPoisoned. 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
Shareableimbriquées sont stockés puis relus. Les closures, les ressources et les objets PHP nonShareablelèventTypeException.
getOrInit() depuis l'intérieur de sa propre factory lève DeadlockException. Restructurez pour que l'appel interne utilise un Once différent.
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=Nexpose{ status: "uninitialized" | "pending" | "ready" | "poisoned", type: "Once" }ainsi qu'un aperçu de la valeur stockée lorsqu'elle estready.
Quand ne pas l'utiliser
- Des valeurs qui changent après leur création.
Onces'écrit une seule fois. UtilisezShared\MutexouShared\Maplorsque 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).