Shared\Once

OxPHP\Shared\Once виконує замикання ініціалізації рівно один раз для однієї комірки Once і робить його результат видимим для всіх наступних викликів цієї ж комірки. Це примітив для випадку «дорога операція, яка має виконатися щонайбільше один раз».

Щоб отримати справжню семантику «рівно один раз на весь процес» між воркерами й запитами, прив’яжіть Once до імені через Shared\Registry::once(...) — тоді всі воркери сходяться на одній комірці. Простий виклик конструктора new Shared\Once() створює окрему комірку для кожного потоку воркера в режимі воркерів (конструктор виконується під час bootstrap кожного воркера) і для кожного запиту в традиційному режимі — це дає «один раз на комірку», а не «один раз на процес».

Огляд

  • Одноразове виконання між воркерами для однієї комірки. Якщо два воркери одночасно викликають getOrInit($factory) на тій самій комірці Once, фабрика виконається лише в одному з них; той, хто програв перегони, блокується й отримує значення переможця. Поєднуйте з Shared\Registry::once, щоб «та сама комірка» означала «те саме ім’я в усіх воркерах».
  • Скінченний автомат із чотирма станами. Комірка перебуває в стані Uninitialized, Pending (фабрика виконується саме зараз), Ready або Poisoned. Прочитати стан можна через status().
  • Жодного двозначного null. get() кидає виняток на незаповненій комірці замість повернення null, тож збережений null — це справжнє значення, а не «відсутність».
  • Безпечність щодо повторного входу. Виклик getOrInit() на тому самому Once зсередини його власної фабрики кидає DeadlockException, а не зависає.
  • Налаштовувана політика збоїв. За замовчуванням невдала фабрика скидає комірку, щоб пізніший виклик міг повторити спробу. Увімкніть Poison, щоб невдала фабрика вимикала комірку назавжди.
  • Можливість спільного використання. Екземпляри живуть у реєстрі й передаються через захоплення use та записи Shared\Map.

Довідник 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; }
Метод Повертає Сценарій використання
get збережене значення Прочитати значення, яке точно в стані Ready. Кидає виняток для uninit / pending / poison.
status Once\Status Інтроспекція / діагностика. Ніколи не кидає винятків (безпечний спосіб побачити poison).
trySet чи переміг? Push-ініціалізація значенням, яке вже є під рукою (без ресурсів із побічними ефектами).
getOrInit збережене значення Pull-ініціалізація; канонічний примітив без перегонів.
id ідентифікатор у реєстрі Кореляція в логах / спостережуваності.

Приклади

Дорога конфігурація, завантажена один раз на процес

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() — це патерн, стійкий до cache stampede: під сплеском одночасних перших звернень фабрика виконується рівно один раз, якщо вона завершується успішно, і кожен викликач — зокрема ті, хто програв перегони, — отримує значення переможця. Якщо фабрика-переможець кидає виняток у режимі Reset, наступний заблокований викликач стає ініціалізатором і повторює спробу, тож фабрика, що постійно падає під навантаженням, повторюється послідовно, а не розгалужується паралельно. Використовуйте режим Poison (нижче), коли збій має бути остаточним.

Розгалуження за станом без запуску ініціалізації

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() призначений для інтроспекції — він ніколи не запускає фабрику й ніколи не кидає винятків, навіть на «отруєній» комірці. Щоб справді отримати значення без перегонів, викликайте getOrInit().

Ініціалізація значенням, коли воно вже відоме

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

Використовуйте trySet() лише для значень, отримання яких не має побічних ефектів. Для ресурсів (з’єднань, файлових дескрипторів, сокетів) використовуйте getOrInit(): trySet(), що програв перегони, просто віддає звичайне значення збирачеві сміття, а ресурс, отриманий до програних перегонів, витече.

Повернення false означає, що комірка вже була в стані Ready або Pending — це не гарантує, що пізніший get() завершиться успішно, адже фабрика в стані Pending в іншому потоці ще може впасти й скинути комірку (у режимі Reset). Не пишіть if (!$o->trySet($v)) { $x = $o->get(); }; якщо вам потрібне значення, викликайте getOrInit().

Початкове з’єднання з базою даних

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

Для пулу з’єднань із кількома слотами дивіться Shared\Pool — Once дає одне значення, а Pool — N.

Швидка відмова через зламану передумову

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

Семантика та підводні камені

  • get() кидає виняток, коли комірка не в стані Ready. UninitializedException — для порожньої комірки або комірки в стані Pending, PoisonedException — для «отруєної». Використовуйте status(), щоб розгалужуватися без винятків, або getOrInit(), щоб безпечно отримати значення.
  • Фабрика виконується щонайбільше один раз на кожну успішну ініціалізацію. Паралельні викликачі блокуються на переможці; власної копії фабрики вони не запускають.
  • Політика збоїв задається під час створення, а не для кожного виклику. Reset (за замовчуванням) повертає комірку в Uninitialized після збою фабрики, щоб пізніший виклик повторив спробу; Poison переводить комірку в остаточний стан Poisoned. В обох режимах виняток фабрики повторно кидається поточному викликачеві.
  • Фатальна помилка всередині фабрики скидає комірку в обох режимах. Фатальна помилка (зокрема вичерпання max_execution_time під час роботи фабрики) завершує запит, а не спричиняє збій фабрики, тож «отруювати» нічого. Комірка повертається в Uninitialized навіть у режимі Poison, і наступний getOrInit запускає власну фабрику. Сама фатальна помилка однаково завершує запит, і її не можна перехопити.
  • Повний діапазон значень. Скаляри, масиви й вкладені значення Shareable зберігаються й читаються назад. Замикання, ресурси та PHP-об’єкти, що не є Shareable, спричиняють TypeException.
Повторний вхід кидає виняток

getOrInit() зсередини власної фабрики спричиняє DeadlockException. Перебудуйте код так, щоб внутрішній виклик використовував інший Once.

Poison чесний між потоками, але не ідентичний за об’єктом

Об’єкт винятку PHP не може перетинати межі потоків воркерів, тому «отруєна» комірка зберігає клас, повідомлення й код збою. Пізніші викликачі в будь-якому потоці отримують новий PoisonedException із цією інформацією — ті самі подробиці, але не той самий об’єкт.

Винятки

Виняток Коли виникає
UninitializedException get() на комірці в стані Uninitialized або Pending.
PoisonedException get() / getOrInit() / trySet() на комірці в стані Poisoned.
DeadlockException getOrInit() рекурсивно викликано на тому самому Once з його фабрики.
TypeException Збережене значення не серіалізується (замикання, ресурс).
StaleHandleException Будь-який метод на дескрипторі, запис якого витіснено з реєстру.

Якщо сама фабрика кидає виняток, він без змін передається поточному викликачеві. У режимі Reset комірка залишається неініціалізованою, і наступний getOrInit повторює спробу; у режимі Poison комірка стає «отруєною».

Спостережуваність

Дивіться Спостережуваність спільного стану. Коротка довідка:

  • GET /__ox_shared/entry?id=N показує { status: "uninitialized" | "pending" | "ready" | "poisoned", type: "Once" }, а також попередній перегляд збереженого значення в стані ready.

Коли не варто використовувати

  • Значення, що змінюються після створення. Once записується лише один раз. Використовуйте Shared\Mutex або Shared\Map, коли збережений стан змінюється.
  • Локальний стан окремого воркера. Статичні властивості класу або глобальні змінні модуля дешевші, коли значення не потрібно спільно використовувати.
  • Дорогі обчислення для кожного запиту. Кешуйте в межах запиту, а не в спільному стані — інакше матимете витік пам’яті.

Промпти для вашого агента

Ці промпти розраховані на агента з підключеним MCP-сервером документації OxPHP — потрібні сторінки він прочитає сам.

prompt
Використай OxPHP Shared Once у моєму коді замість лінивого патерну `static $x ??= init()`, який перезапускає свою фабрику один раз на кожен потік воркера, а не один раз на процес. Прочитай сторінку `shared-state/shared-once` через MCP-сервер документації OxPHP. Знайди цю ініціалізацію в репозиторії й покажи мені заміну на `Registry::once`, щоб витрати на налаштування сплачував лише один воркер.

Пов’язані сторінки

  • Спільний стан — огляд і ментальна модель.
  • Shared\Mutex — коли одноразове значення згодом змінюється.
  • Shared\Pool — одноразова ініціалізація N рівноцінних ресурсів.
  • Shared\Map — ініціалізація за ключем через getOrSet($key, $factory).