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órceOnceuruchamiają fabrykę tylko w jednym z nich; przegrany blokuje się i otrzymuje wartość zwycięzcy. Połącz zShared\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),ReadylubPoisoned. Odczytaj go za pomocąstatus(). - Bez niejednoznacznego null.
get()rzuca wyjątek na nieustawionej komórce zamiast zwracaćnull, więc zapisanynulljest 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 rzucaDeadlockExceptionzamiast 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
useoraz wpisyShared\Map.
Dokumentacja 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; }| 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
// 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
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
$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 aboveUż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 Pending — nie 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
// 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\Pool — Once daje jedną wartość; Pool daje N.
Szybka porażka przy zepsutym warunku wstępnym
<?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 jestReady.UninitializedExceptiondla pustej komórki lub komórki w staniePending,PoisonedExceptiondla zatrutej. Użyjstatus(), aby rozgałęzić się bez wyjątku, lubgetOrInit(), 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 stanuUninitializedprzy niepowodzeniu fabryki, tak aby późniejsze wywołanie ponowiło próbę;Poisonczyni komórkę trwalePoisoned. 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
Shareablesą zapisywane i odczytywane z powrotem. Domknięcia, zasoby i obiekty PHP niebędąceShareablepowodująTypeException.
getOrInit() z wnętrza jego własnej fabryki rzuca DeadlockException. Przebuduj kod tak, aby wewnętrzne wywołanie używało innego Once.
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=Nudostępnia{ status: "uninitialized" | "pending" | "ready" | "poisoned", type: "Once" }oraz podgląd zapisanej wartości, gdyready.
Kiedy nie używać
- Wartości, które zmieniają się po utworzeniu.
Oncejest zapisywany jednokrotnie. UżyjShared\MutexlubShared\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).