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&lt;E&gt;) }<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

worker.php
<?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

php
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.

Warning

Przechwycone uchwyty do poprzedniego powiązania nadal operują na starym (teraz anonimowym) wpisie; nie zbiegają się automatycznie na nowym.

php
$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 empty

Jeś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ówne count(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ą ogranicza SHARED_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 limitami SHARED_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ż