Shared\Registry
OxPHP\Shared\Registry est le compagnon indexé par nom du reste de OxPHP\Shared\*. Là où new Shared\Map() produit une entrée anonyme partagée uniquement par propagation de handle (capture use, Fibers asynchrones, imbrication), Registry::map('cache', fn() => new Shared\Map(...)) lie une entrée sous une clé chaîne. Chaque appelant de Registry::map('cache', …), sur n'importe quel thread worker, dans n'importe quelle requête, obtient la même entrée.
Il répond à une seule question : « comment partager un même Shared\Map entre tous les workers, ou entre toutes les requêtes en mode traditionnel ? » Les autres types Shared\* restent la bonne unité d'état mutable ; Registry est simplement la façon de donner un nom à l'un d'eux.
Modèle mental
graph TD
R["Registry::map('cache', $factory)"]
W1["worker #1"] --> R
W2["worker #2"] --> R
W3["worker #3"] --> R
R --> S["SharedRegistry (process-global)<br/>names: { 'cache' → Bound(Arc<E>) }<br/>entries: { id=7: Map { … } }"]
- Le premier appelant de
Registry::map($key, $factory)pour une clé non liée exécute la factory et épingle l'entrée obtenue sous ce nom. - Chaque appelant suivant (même thread, autres workers, requêtes ultérieures) reçoit la même entrée. La factory n'est pas ré-exécutée ; elle est ignorée en cas de hit.
- Les premiers accès concurrents se bloquent sur une barrière par clé : un seul thread exécute la factory, les autres attendent et récupèrent l'entrée du gagnant. Cela évite une double acquisition de ressources pour les pools de connexions.
L'identité par nom complète l'identité par handle. Les entrées anonymes (new Shared\*()) et nommées coexistent dans le même registre process-global. L'index des noms ajoute par-dessus une recherche par chaîne.
Démarrage rapide — un compteur pour tous les workers
<?php
// worker.php — entry script in worker mode, executed once per worker thread
require __DIR__ . '/vendor/autoload.php';
$requests = OxPHP\Shared\Registry::counter(
'request-counter',
fn() => new OxPHP\Shared\Counter(),
);
oxphp_worker(function () use ($requests) {
$n = $requests->add(); // atomic across ALL workers — one shared int64
header('X-Request-Count: ' . $n);
echo "hello\n";
});À comparer au modèle du handle capturé ($x = new Shared\Counter() dans le bootstrap). Ce modèle produit un compteur par thread worker : chaque worker exécute son propre bootstrap et obtient sa propre entrée anonyme. Les totaux agrégés divergent d'un facteur égal à la taille du pool de workers. Registry::counter('request-counter', …) fait au contraire converger chaque worker sur une seule entrée, si bien que le compte correspond au total réel.
La même forme fonctionne en mode traditionnel (sans WORKER_MODE_ENABLED). La première requête qui touche 'request-counter' crée l'entrée ; chaque requête suivante (sur n'importe quel thread worker) la voit. C'est le scénario du remplacement d'APCu sur un même hôte, avec des primitives typées et des opérations atomiques à la place de apcu_fetch / apcu_store.
Référence de l'API
namespace OxPHP\Shared;
final class Registry
{
// Typed get-or-create. On hit, the factory is ignored; on miss it
// runs at most once across all workers (block-losers) and must
// return a fresh instance of the matching type.
public static function map(string $key, callable $factory): Map;
public static function counter(string $key, callable $factory): Counter;
public static function atomic(string $key, callable $factory): Atomic;
public static function flag(string $key, callable $factory): Flag;
public static function once(string $key, callable $factory): Once;
public static function mutex(string $key, callable $factory): Mutex;
public static function channel(string $key, callable $factory): Channel;
public static function pool(string $key, callable $factory): Pool;
// Untyped escape hatch — returns whatever is bound (no type guard).
public static function global(string $key, callable $factory): Shareable;
// Namespace management — operates on the name index, NOT the objects.
public static function remove(string $key): bool;
public static function keys(): array; // list<string>
// Layer-wide introspection.
public static function memoryUsage(): int; // estimated bytes, all Shared\* entries
public static function count(): int; // live Shared\* entries (named + anonymous)
}| Méthode | Renvoie | Usage |
|---|---|---|
map / counter / atomic / flag / once / mutex / channel / pool |
le type Shared\* demandé |
Surface principale. Type vérifié en cas de hit ; type validé au retour de la factory. |
global |
Shareable |
get-or-create non typé. À n'utiliser que lorsque vous ne connaissez vraiment pas le type lié à l'avance. |
remove |
bool |
Supprime la liaison de nom + l'épinglage. Ne détruit pas l'objet. |
keys |
list<string> |
Clés actuellement liées (Bound uniquement ; les emplacements Creating en cours ne sont pas listés). |
memoryUsage |
int |
Octets estimés à l'échelle du processus — voir Mémoire et introspection. |
count |
int |
Entrées vivantes à l'échelle du processus (nommées et anonymes). |
Registry est une façade statique : new Registry() lève Shared\SharedException.
Cycle de vie — épinglé par défaut
Une clé liée détient une référence forte vers son entrée ; l'entrée reste en vie pendant toute la durée de vie du processus, sauf si vous appelez explicitement remove(key) ou si le processus s'arrête. C'est délibéré : en mode traditionnel, où chaque requête crée ses propres handles côté PHP qui meurent à la fin de la requête, l'épinglage de l'index des noms est la seule raison pour laquelle l'entrée survit entre les requêtes.
Invalidez le contenu d'une entrée nommée en la mutant sur place ($cache->clear(), $counter->set(0), $bucket->remove($k)), et non en supprimant le nom. La mutation est partagée par référence : chaque détenteur de la même clé voit le changement immédiatement.
remove gère l'espace de noms, ce n'est pas une destruction d'objet
remove($key) supprime la liaison et l'épinglage. L'entrée elle-même survit tant qu'un autre handle la référence (une variable capturée dans le bootstrap, une valeur imbriquée dans un autre Shared\Map, un oxphp_async en cours). Quand le dernier handle disparaît, l'entrée se désenregistre d'elle-même, comme d'habitude.
Après remove, la clé est libre. Le prochain Registry::map($key, …) crée une nouvelle entrée avec un id distinct.
Les handles capturés vers la liaison précédente continuent d'opérer sur l'ancienne entrée (désormais anonyme) ; ils ne convergent pas automatiquement vers la nouvelle.
$cache = Registry::map('cache', fn() => new Shared\Map());
$id_a = $cache->id();
Registry::remove('cache');
$cache->set('x', 1); // still mutates the OLD entry — fine, but it's no longer "cache"
$fresh = Registry::map('cache', fn() => new Shared\Map());
$id_b = $fresh->id(); // different id — this is a new entry
assert($id_a !== $id_b);
assert($cache->get('x') === 1); // OLD entry retained value
assert($fresh->get('x') === null); // NEW entry is emptySi vous faites tourner les clés (entrées par tenant qui vont et viennent, versionnement de clés), adressez-les par nom à chaque appel (Registry::map($key, …) par requête) plutôt que de capturer le handle une seule fois dans le bootstrap. Handles capturés + rotation de clés divergent silencieusement ; l'adressage par nom converge vers la liaison actuellement en vigueur.
remove renvoie true si une clé liée a été supprimée, false si la clé était absente.
Erreurs
| Exception | Quand |
|---|---|
Shared\TypeException |
Méthode typée sur une clé liée à un type différent ; la factory a renvoyé le mauvais type Shared\* ou une valeur non-Shareable. |
Shared\CapacityException |
La création dépasserait les limites SHARED_MAX_ENTRIES / SHARED_MAX_BYTES. |
Shared\DeadlockException (réentrant) |
Registry::map($key, …) pour le même $key depuis l'intérieur de sa propre factory, sur le même thread. |
Shared\DeadlockException (cycle inter-clés) |
Attente de plus de 30 s sur l'emplacement Creating d'un autre thread — le plus vraisemblablement, la factory A détient la clé K1 tout en attendant K2, dont la factory est détenue par le thread B qui attend K1. Message distinct de celui du cas réentrant. |
Shared\SharedException (draining) |
Le serveur est en cours d'arrêt — le registre refuse les nouvelles acquisitions et liaisons. Attendu lors d'un arrêt gracieux ; ce n'est pas un bug de code. |
Shared\SharedException (bind race) |
Un créateur concurrent s'était déjà installé dans l'emplacement pendant que la factory de ce thread s'exécutait (l'entrée de la factory n'a PAS été épinglée sous la clé). Réessayez l'appel. |
\InvalidArgumentException (SPL) |
$key vide. Validation d'argument, distincte des erreurs de type métier. |
| (exception de la factory) | Si la factory lève une exception, l'emplacement est abandonné (Creating → absent, les threads en attente se réveillent pour réessayer) et l'exception d'origine est propagée au créateur. |
Shared\DeadlockException étend OxPHP\Async\AsyncException, si bien que catch (AsyncException) le capture en même temps que les délais d'expiration d'attente bornée présents ailleurs dans Shared\*. Les deux cas distincts de DeadlockException partagent la classe ; distinguez-les par le message ("reentrant get-or-create" contre "waited too long … cross-key cycle").
Mémoire et introspection
Registry::memoryUsage() et Registry::count() rendent compte de toute la couche Shared*, pas seulement des entrées nommées. Les entrées anonymes créées via new Shared\*() (l'essentiel de l'usage actuel de Shared\* : captures dans le bootstrap, valeurs en cours à l'intérieur de Map et Channel, captures dans les Fibers asynchrones) y sont incluses.
C'est délibéré. Ces deux nombres existent pour le monitoring de capacité / OOM ; ce monitoring doit voir l'état anonyme par worker et en cours, pas seulement l'espace de noms nommé. En conséquence :
- Les deux nombres sont transitoires : ils montent et descendent avec les requêtes en cours et les handles par worker.
Registry::count()n'est pas égal àcount(Registry::keys()).keys()ne concerne que l'espace de noms nommé.memoryUsage()est une estimation comptable statique, pas le RSS réel. C'est le même nombre que celui que plafonneSHARED_MAX_BYTES. Pour l'empreinte réelle du tas, utilisez un profileur de tas (heaptrack,jemalloc_stats_print,mi_stats_print) ou les métriques mémoire du conteneur.
Le détail par entrée (id, type, compteur de références, coût en octets) se trouve sur l'endpoint d'introspection interne à /__ox_shared/entries. Il n'y a volontairement aucune API PHP par entrée, afin d'éviter de dupliquer cette surface.
Quand ne pas l'utiliser
- Entre processus, entre hôtes. Le registre vit à l'intérieur d'un seul processus OxPHP. Plusieurs instances OxPHP ne le partagent pas. Utilisez Redis / NATS / votre broker existant ; voir Migrer vers un magasin externe.
- Durabilité entre redémarrages. Le registre s'évapore à la sortie du processus. Persistez les données via le même magasin externe.
- Clés éphémères à fort renouvellement. La sémantique « épinglé par défaut » signifie que les clés dynamiques générées à chaque requête font fuir des entrées tant que vous n'appelez pas
remove. C'est borné par les limitesSHARED_MAX_*, mais reste une mauvaise pratique. Pour un état de courte durée propre à la requête, utilisez une variable PHP normale. - Une primitive d'invalidation de cache.
remove($key)sert à retirer un nom, pas à « vider le cache ». Invalidez le contenu sur place ($map->clear(),$map->remove($member_key)) ; la liaison de nom survit.
Voir aussi
- État partagé. Vue d'ensemble de la couche, identité par handle, et quand
new Shared\*()est le bon outil. - Shared\Map, Shared\Counter, Shared\Pool, et les autres primitives typées que renvoie
Registry. - Observabilité de l'état partagé. L'API JSON
/__ox_shared/*et les métriques Prometheus. - Migrer vers un magasin externe. Quand un seul processus ne vous suffit plus.