Shared\Once

OxPHP\Shared\Once uruchamia domknięcie inicjalizujące dokładnie raz dla pojedynczej komórki Once i udostępnia jego wynik każdemu kolejnemu wywołującemu tę samą komórkę. To prymityw dla „kosztownej operacji, która powinna wydarzyć się co najwyżej raz”.

Aby uzyskać prawdziwą semantykę „dokładnie raz w obrębie całego procesu” działającą między workerami i między żądaniami, powiąż Once z nazwą za pomocą Shared\Registry::once(...), tak aby wszystkie workery zbiegały się na tej samej komórce. Sam wzorzec konstruktora new Shared\Once() tworzy osobną komórkę na każdy wątek workera w trybie worker (bootstrap każdego workera uruchamia konstruktor) oraz na każde żądanie w trybie tradycyjnym — daje to „raz na komórkę”, a nie „raz na proces”.

Przegląd

  • Wykonanie jednorazowe między workerami dla jednej komórki. Dwa workery ścigające się w getOrInit($factory) na tej samej komórce Once uruchamiają fabrykę tylko w jednym z nich; przegrany blokuje się i otrzymuje wartość zwycięzcy. Połącz z Shared\Registry::once, aby „ta sama komórka” oznaczała „tę samą nazwę we wszystkich workerach”.
  • Maszyna o czterech stanach. Komórka jest w stanie Uninitialized, Pending (fabryka właśnie się wykonuje), Ready lub Poisoned. Odczytaj go za pomocą status().
  • Bez niejednoznacznego null. get() rzuca wyjątek na nieustawionej komórce zamiast zwracać null, więc zapisany null jest prawdziwą wartością, a nie „brakiem”.
  • Bezpieczeństwo przy ponownym wejściu. Wywołanie getOrInit() na tym samym Once z wnętrza jego własnej fabryki rzuca DeadlockException zamiast się zawieszać.
  • Konfigurowalna polityka błędów. Domyślnie nieudana fabryka resetuje komórkę, tak aby późniejsze wywołanie mogło ponowić próbę. Włącz Poison, aby nieudana fabryka trwale wyłączała komórkę.
  • Możliwość współdzielenia. Instancje żyją w rejestrze i przenoszą się przez przechwycenia use oraz wpisy Shared\Map.

Dokumentacja 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; }
Metoda Zwraca Zastosowanie
get zapisana wartość Odczyt wartości, o której wiesz, że jest Ready. Rzuca wyjątek przy uninit / pending / poison.
status Once\Status Introspekcja / diagnostyka. Nigdy nie rzuca (bezpieczny obserwator zatrutej komórki).
trySet zwycięzca? Inicjalizacja w modelu push dla wartości już posiadanej (zasób bez efektów ubocznych).
getOrInit zapisana wartość Inicjalizacja w modelu pull; kanoniczny prymityw wolny od wyścigów.
id id rejestru Korelacja dla logowania / obserwowalności.

Przykłady

Kosztowna konfiguracja wczytywana raz na proces

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() to wzorzec bezpieczny wobec cache stampede: przy nagłym natłoku równoczesnych pierwszych dostępów fabryka uruchamia się dokładnie raz gdy się powiedzie, a każdy wywołujący — w tym ci, którzy przegrali wyścig — otrzymuje wartość zwycięzcy. Jeśli zwycięska fabryka rzuci wyjątek w trybie Reset, następny zablokowany wywołujący staje się inicjalizatorem i ponawia próbę, więc uporczywie zawodząca fabryka pod obciążeniem ponawia próby szeregowo, zamiast rozgałęziać się równolegle. Użyj trybu Poison (poniżej), gdy błąd ma być zamiast tego terminalny.

Rozgałęzianie według stanu bez wyzwalania inicjalizacji

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() służy do introspekcji — nigdy nie wyzwala fabryki i nigdy nie rzuca wyjątku, nawet na zatrutej komórce. Aby faktycznie uzyskać wartość w sposób wolny od wyścigów, wywołaj getOrInit().

Inicjalizacja od wartości, gdy wartość jest już znana

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

Używaj trySet() tylko dla wartości, których pozyskanie nie ma efektów ubocznych. Dla zasobów (połączenia, uchwyty plików, gniazda) używaj zamiast tego getOrInit(): trySet(), który przegrywa wyścig, jedynie oddaje zwykłą wartość garbage collectorowi, ale zasób pozyskany przed przegranym wyścigiem wyciekłby.

Zwrot false oznacza, że komórka była już Ready lub Pendingnie gwarantuje to, że późniejsze get() się powiedzie, ponieważ fabryka w stanie Pending na innym wątku wciąż może zawieść i zresetować komórkę (w trybie Reset). Nie pisz if (!$o->trySet($v)) { $x = $o->get(); }; jeśli potrzebujesz wartości, wywołaj getOrInit().

Bootstrap połączenia z bazą danych

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, ]); });

Aby uzyskać pulę połączeń z wieloma slotami, zobacz Shared\PoolOnce daje jedną wartość; Pool daje N.

Szybka porażka przy zepsutym warunku wstępnym

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());

Semantyka i pułapki

  • get() rzuca wyjątek, gdy komórka nie jest Ready. UninitializedException dla pustej komórki lub komórki w stanie Pending, PoisonedException dla zatrutej. Użyj status(), aby rozgałęzić się bez wyjątku, lub getOrInit(), aby bezpiecznie uzyskać wartość.
  • Fabryka uruchamia się co najwyżej raz na udaną inicjalizację. Równoczesni wywołujący blokują się na zwycięzcy; nie uruchamiają własnej kopii.
  • Polityka błędów jest ustawiana przy konstrukcji, a nie per wywołanie. Reset (domyślnie) przywraca komórkę do stanu Uninitialized przy niepowodzeniu fabryki, tak aby późniejsze wywołanie ponowiło próbę; Poison czyni komórkę trwale Poisoned. W obu trybach wyjątek fabryki jest rzucany ponownie do bieżącego wywołującego.
  • Pełny zakres wartości. Skalary, tablice i zagnieżdżone wartości Shareable są zapisywane i odczytywane z powrotem. Domknięcia, zasoby i obiekty PHP niebędące Shareable powodują TypeException.
Ponowne wejście rzuca wyjątek

getOrInit() z wnętrza jego własnej fabryki rzuca DeadlockException. Przebuduj kod tak, aby wewnętrzne wywołanie używało innego Once.

Poison jest uczciwy między wątkami, ale nie zachowuje tożsamości obiektu

Obiekt wyjątku PHP nie może przekraczać granic wątków workerów, więc zatruta komórka zapamiętuje klasę, komunikat i kod błędu. Późniejsi wywołujący na dowolnym wątku otrzymują świeży PoisonedException niosący te informacje — te same szczegóły, ale nie ten sam obiekt.

Wyjątki

Wyjątek Rzucany przez
UninitializedException get() na komórce Uninitialized lub Pending.
PoisonedException get() / getOrInit() / trySet() na komórce Poisoned.
DeadlockException getOrInit() wywołane rekurencyjnie na tym samym Once z jego fabryki.
TypeException Zapisywana wartość nie jest serializowalna (domknięcie, zasób).
StaleHandleException Dowolna metoda na uchwycie, którego wpis w rejestrze został usunięty.

Jeśli sama fabryka rzuci wyjątek, propaguje on bez zmian do bieżącego wywołującego. W trybie Reset komórka pozostaje niezainicjalizowana, a następne getOrInit ponawia próbę; w trybie Poison komórka staje się zatruta.

Obserwowalność

Zobacz Obserwowalność stanu współdzielonego. Krótkie odniesienia:

  • GET /__ox_shared/entry?id=N udostępnia { status: "uninitialized" | "pending" | "ready" | "poisoned", type: "Once" } oraz podgląd zapisanej wartości, gdy ready.

Kiedy nie używać

  • Wartości, które zmieniają się po utworzeniu. Once jest zapisywany jednokrotnie. Użyj Shared\Mutex lub Shared\Map, gdy zapisany stan się zmienia.
  • Stan lokalny per worker. Statyczne właściwości klas lub globalne zmienne modułu są tańsze, gdy wartość nie musi być współdzielona.
  • Kosztowne obliczenia per żądanie. Buforuj w obrębie żądania, a nie w stanie współdzielonym — inaczej doprowadzisz do wycieku pamięci.

Powiązane

  • Stan współdzielony — przegląd i model myślowy.
  • Shared\Mutex — gdy jednorazowa wartość później się zmienia.
  • Shared\Pool — jednorazowa inicjalizacja N równoważnych zasobów.
  • Shared\Map — inicjalizacja z kluczem za pomocą getOrSet($key, $factory).