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
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
// 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
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
$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
// 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
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.
Об’єкт винятку 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 — потрібні сторінки він прочитає сам.
Використай OxPHP Shared Once у моєму коді замість лінивого патерну `static $x ??= init()`, який перезапускає свою фабрику один раз на кожен потік воркера, а не один раз на процес. Прочитай сторінку `shared-state/shared-once` через MCP-сервер документації OxPHP.
Знайди цю ініціалізацію в репозиторії й покажи мені заміну на `Registry::once`, щоб витрати на налаштування сплачував лише один воркер.Пов’язані сторінки
- Спільний стан — огляд і ментальна модель.
- Shared\Mutex — коли одноразове значення згодом змінюється.
- Shared\Pool — одноразова ініціалізація N рівноцінних ресурсів.
- Shared\Map — ініціалізація за ключем через
getOrSet($key, $factory).