Shared\Registry
OxPHP\Shared\Registry to kluczowany nazwą towarzysz reszty OxPHP\Shared\*. Podczas gdy new Shared\Map() tworzy anonimowy wpis współdzielony wyłącznie przez propagację uchwytu (przechwycenie przez use, asynchroniczne Fibery, zagnieżdżanie), Registry::map('cache', fn() => new Shared\Map(...)) wiąże wpis pod kluczem tekstowym. Każdy wywołujący Registry::map('cache', …), na dowolnym wątku workera, w dowolnym żądaniu, otrzymuje ten sam wpis.
Odpowiada na jedno pytanie: „jak współdzielić jedną Shared\Map między wszystkimi workerami albo między wszystkimi żądaniami w trybie tradycyjnym?” Pozostałe typy Shared\* nadal są właściwą jednostką zmiennego stanu; Registry to jedynie sposób, w jaki nadajesz jednemu z nich nazwę.
Model myślowy
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 { … } }"]
- Pierwszy wywołujący
Registry::map($key, $factory)dla niezwiązanego klucza uruchamia fabrykę i przypina wynikowy wpis pod tą nazwą. - Każdy kolejny wywołujący (ten sam wątek, inne workery, późniejsze żądania) otrzymuje ten sam wpis. Fabryka nie jest uruchamiana ponownie; przy trafieniu jest ignorowana.
- Współbieżne pierwsze dotknięcia blokują się na bramce per-klucz: dokładnie jeden wątek uruchamia fabrykę, pozostałe czekają i otrzymują wpis zwycięzcy. Zapobiega to podwójnemu pozyskaniu zasobu w przypadku pul połączeń.
Tożsamość-przez-nazwę uzupełnia tożsamość-przez-uchwyt. Wpisy anonimowe (new Shared\*()) i nazwane współistnieją w tym samym procesowo-globalnym rejestrze. Indeks nazw dokłada wyszukiwanie po ciągu znaków na wierzchu.
Szybki start — jeden licznik dla każdego workera
<?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";
});Porównaj to ze wzorcem przechwyconego uchwytu ($x = new Shared\Counter() w bootstrapie). Ten wzorzec tworzy jeden licznik na wątek workera: każdy worker uruchamia własny bootstrap i otrzymuje własny anonimowy wpis. Zagregowane zliczenia rozjeżdżają się o współczynnik równy rozmiarowi puli workerów. Registry::counter('request-counter', …) zamiast tego zbiega każdego workera na jednym wpisie, więc zliczenie odpowiada faktycznej sumie.
Ten sam kształt działa w trybie tradycyjnym (bez WORKER_MODE_ENABLED). Pierwsze żądanie, które dotyka 'request-counter', tworzy wpis; każde kolejne żądanie (na dowolnym wątku workera) go widzi. To ta sama historia zastąpienia APCu w obrębie jednego hosta, tyle że z typowanymi prymitywami i operacjami atomowymi zamiast apcu_fetch / apcu_store.
Dokumentacja 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)
}| Metoda | Zwraca | Zastosowanie |
|---|---|---|
map / counter / atomic / flag / once / mutex / channel / pool |
żądany typ Shared\* |
Główna powierzchnia. Chroniona typem przy trafieniu; walidowana typem przy zwrocie fabryki. |
global |
Shareable |
Nietypowane get-or-create. Sięgaj po nie tylko wtedy, gdy naprawdę nie znasz z góry związanego typu. |
remove |
bool |
Usuwa powiązanie nazwy + przypięcie. Nie niszczy obiektu. |
keys |
list<string> |
Aktualnie związane klucze (tylko Bound; sloty Creating w trakcie tworzenia nie są wymieniane). |
memoryUsage |
int |
Szacowane bajty w skali procesu — zobacz Pamięć i introspekcja. |
count |
int |
Żywe wpisy w skali procesu (nazwane i anonimowe). |
Registry to statyczna fasada: new Registry() rzuca Shared\SharedException.
Cykl życia — domyślnie przypięty
Związany klucz utrzymuje silną referencję do swojego wpisu; wpis żyje przez cały czas życia procesu, chyba że jawnie wywołasz remove(key) albo proces zostanie zburzony. Jest to celowe: w trybie tradycyjnym, gdzie każde żądanie tworzy własne uchwyty po stronie PHP i giną one na końcu żądania, przypięcie w indeksie nazw jest jedynym powodem, dla którego wpis przetrwa między żądaniami.
Unieważniaj zawartość nazwanego wpisu, modyfikując go w miejscu ($cache->clear(), $counter->set(0), $bucket->remove($k)), a nie usuwając nazwę. Modyfikacja jest współdzielona przez referencję: każdy posiadacz tego samego klucza widzi zmianę natychmiast.
remove to zarządzanie przestrzenią nazw, a nie niszczenie obiektu
remove($key) usuwa powiązanie i przypięcie. Sam wpis przetrwa, dopóki jakikolwiek inny uchwyt się do niego odwołuje (przechwycona zmienna z bootstrapu, wartość zagnieżdżona w innej Shared\Map, oxphp_async w trakcie wykonywania). Gdy ostatni uchwyt zniknie, wpis wyrejestrowuje się sam jak zwykle.
Po remove klucz jest wolny. Następne Registry::map($key, …) tworzy nowy wpis z odrębnym id.
Przechwycone uchwyty do poprzedniego powiązania nadal operują na starym (teraz anonimowym) wpisie; nie zbiegają się automatycznie na nowym.
$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 emptyJeśli rotujesz klucze (wpisy per-tenant, które pojawiają się i znikają, wersjonowanie kluczy), adresuj je po nazwie w każdym wywołaniu (Registry::map($key, …) na żądanie) zamiast przechwytywać uchwyt raz w bootstrapie. Przechwycone uchwyty + rotacja kluczy rozjeżdżają się po cichu; adresowanie po nazwie zbiega się na aktualnym powiązaniu, jakiekolwiek by nie było.
remove zwraca true, jeśli związany klucz został usunięty, false, jeśli klucza nie było.
Błędy
| Wyjątek | Kiedy |
|---|---|
Shared\TypeException |
Typowana metoda na kluczu związanym z innym typem; fabryka zwróciła zły typ Shared\* lub coś, co nie jest Shareable. |
Shared\CapacityException |
Utworzenie przekroczyłoby limity SHARED_MAX_ENTRIES / SHARED_MAX_BYTES. |
Shared\DeadlockException (reentrancja) |
Registry::map($key, …) dla tego samego $key z wnętrza jego własnej fabryki na tym samym wątku. |
Shared\DeadlockException (cykl między kluczami) |
Oczekiwanie dłuższe niż 30 s na slot Creating innego wątku — najprawdopodobniej fabryka A trzyma klucz K1, czekając na K2, którego fabryka jest trzymana przez wątek B czekający na K1. Odrębny komunikat od przypadku reentrancji. |
Shared\SharedException (opróżnianie) |
Serwer się wyłącza — rejestr odmawia nowych pozyskań i powiązań. Oczekiwane podczas łagodnego zamknięcia; to nie błąd w kodzie. |
Shared\SharedException (wyścig przy powiązaniu) |
Współbieżny twórca zdążył już osadzić się w slocie, gdy fabryka tego wątku wciąż działała (wpis fabryki NIE został przypięty pod kluczem). Ponów wywołanie. |
\InvalidArgumentException (SPL) |
Pusty $key. Walidacja argumentu, odrębna od domenowych błędów typu. |
| (wyjątek fabryki) | Jeśli fabryka rzuci wyjątek, slot zostaje przerwany (Creating → brak, oczekujący budzą się, by ponowić), a oryginalny wyjątek propaguje do twórcy. |
Shared\DeadlockException rozszerza OxPHP\Async\AsyncException, więc catch (AsyncException) łapie go razem z limitami czasu ograniczonego oczekiwania w innych miejscach Shared\*. Oba odrębne przypadki DeadlockException dzielą tę samą klasę; rozróżnisz je po komunikacie ("reentrant get-or-create" vs "waited too long … cross-key cycle").
Pamięć i introspekcja
Registry::memoryUsage() i Registry::count() raportują całą warstwę Shared*, nie tylko nazwane wpisy. Anonimowe wpisy tworzone przez new Shared\*() (większość obecnego użycia Shared\*: przechwycenia w bootstrapie, wartości w trakcie wykonywania wewnątrz Map i Channel, przechwycenia w asynchronicznych Fiberach) są wliczane.
Jest to celowe. Obie liczby istnieją na potrzeby monitorowania pojemności / OOM; monitorowanie to musi widzieć per-workerowy i przejściowy anonimowy stan, a nie tylko nazwaną przestrzeń nazw. W efekcie:
- Obie liczby są przejściowe: rosną i maleją wraz z żądaniami w toku i per-workerowymi uchwytami.
Registry::count()nie jest równecount(Registry::keys()).keys()to wyłącznie nazwana przestrzeń nazw.memoryUsage()to statyczny szacunek księgowy, a nie faktyczny RSS. To ta sama liczba, którą ograniczaSHARED_MAX_BYTES. Dla rzeczywistego zajęcia sterty użyj profilera sterty (heaptrack,jemalloc_stats_print,mi_stats_print) albo metryk pamięci kontenera.
Szczegóły per-wpis (id, typ, refcount, koszt w bajtach) znajdują się na wewnętrznym endpoincie introspekcji pod /__ox_shared/entries. Celowo nie ma per-wpisowego API w PHP, aby nie duplikować tej powierzchni.
Kiedy nie używać
- Międzyprocesowo, między hostami. Rejestr żyje wewnątrz jednego procesu OxPHP. Wiele instancji OxPHP go nie współdzieli. Użyj Redisa / NATS / swojego istniejącego brokera; zobacz Migracja do zewnętrznego magazynu.
- Trwałość między restartami. Rejestr ulatnia się przy wyjściu procesu. Utrwalaj przez ten sam zewnętrzny magazyn.
- Klucze efemeryczne o dużej rotacji. Semantyka domyślnego przypięcia oznacza, że dynamiczne klucze generowane per-żądanie wyciekają jako wpisy, dopóki nie wywołasz
remove. Ograniczone limitamiSHARED_MAX_*, ale wciąż to zły styl. Dla krótkotrwałego stanu per-żądanie użyj zwykłej zmiennej PHP. - Prymityw unieważniania cache'a.
remove($key)służy do wycofania nazwy, a nie do „wyczyszczenia cache'a”. Unieważniaj zawartość w miejscu ($map->clear(),$map->remove($member_key)); powiązanie nazwy przetrwa.
Zobacz też
- Shared State. Przegląd warstwy, tożsamość-przez-uchwyt oraz kiedy
new Shared\*()jest właściwym narzędziem. - Shared\Map, Shared\Counter, Shared\Pool i pozostałe typowane prymitywy, które zwraca
Registry. - Shared Observability. API JSON
/__ox_shared/*i metryki Prometheusa. - Migracja do zewnętrznego magazynu. Gdy wyrastasz poza jeden proces.