Shared\Registry
OxPHP\Shared\Registry — це іменований компаньйон решти OxPHP\Shared\*. Якщо new Shared\Map() створює анонімний запис, спільний лише завдяки поширенню дескриптора (захоплення через use, асинхронні файбери, вкладеність), то Registry::map('cache', fn() => new Shared\Map(...)) прив’язує запис до рядкового ключа. Кожен, хто викликає Registry::map('cache', …), у будь-якому потоці воркера й у будь-якому запиті, отримує той самий запис.
Він відповідає на одне запитання: «як зробити один Shared\Map спільним для всіх воркерів або для всіх запитів у традиційному режимі?» Решта типів Shared\* і далі залишаються правильною одиницею змінюваного стану; Registry — лише спосіб дати одному з них ім’я.
Ментальна модель
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<E>) }<br/>entries: { id=7: Map { … } }"]
- Перший, хто викликає
Registry::map($key, $factory)для ще не прив’язаного ключа, запускає фабрику й закріплює отриманий запис під цим іменем. - Кожен наступний виклик (у тому самому потоці, в інших воркерах, у пізніших запитах) отримує той самий запис. Фабрика не запускається повторно; при влучанні її ігнорують.
- Одночасні перші звернення блокуються на гейті для кожного ключа: рівно один потік запускає фабрику, решта чекають і отримують запис переможця. Це запобігає подвійному захопленню ресурсів для пулів з’єднань.
Ідентичність за іменем доповнює ідентичність за дескриптором. Анонімні (new Shared\*()) та іменовані записи співіснують в одному процесно-глобальному реєстрі. Індекс імен лише додає поверх нього пошук за рядком.
Швидкий старт — один лічильник для всіх воркерів
<?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";
});Порівняйте з патерном захопленого дескриптора ($x = new Shared\Counter() у бутстрапі). Такий патерн дає окремий лічильник на кожен потік воркера: кожен воркер виконує власний бутстрап і отримує власний анонімний запис. Сумарні значення розходяться в стільки разів, скільки воркерів у пулі. Натомість Registry::counter('request-counter', …) зводить усі воркери до одного запису, тож лічильник показує справжню загальну кількість.
Та сама схема працює й у традиційному режимі (без WORKER_MODE_ENABLED). Перший запит, що звертається до 'request-counter', створює запис; кожен наступний запит (у будь-якому потоці воркера) його бачить. Це і є заміна APCu в межах одного хоста — з типізованими примітивами та атомарними операціями замість apcu_fetch / apcu_store.
Довідник API
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)
}| Метод | Повертає | Призначення |
|---|---|---|
map / counter / atomic / flag / once / mutex / channel / pool |
запитаний тип Shared\* |
Основний інтерфейс. Тип перевіряється при влучанні та при поверненні з фабрики. |
global |
Shareable |
Нетипізоване get-or-create. Вдавайтеся до нього лише тоді, коли справді не знаєте заздалегідь прив’язаного типу. |
remove |
bool |
Скидає прив’язку імені та закріплення. Не знищує об’єкт. |
keys |
list<string> |
Наразі прив’язані ключі (лише Bound; слоти Creating, що ще створюються, не входять). |
memoryUsage |
int |
Оцінка байтів для всього процесу — див. Використання ресурсів та інтроспекція. |
count |
int |
Живі записи в усьому процесі (іменовані та анонімні). |
Registry — статичний фасад: new Registry() кидає Shared\SharedException.
Життєвий цикл — закріплено за замовчуванням
Прив’язаний ключ тримає сильне посилання на свій запис; запис живе весь час існування процесу, якщо ви явно не викличете remove(key) або процес не завершиться. Це зроблено навмисно: у традиційному режимі, де кожен запит створює власні дескриптори на боці PHP і вони зникають наприкінці запиту, закріплення в індексі імен — єдина причина, чому запис переживає проміжок між запитами.
Щоб інвалідувати вміст іменованого запису, змінюйте його на місці ($cache->clear(), $counter->set(0), $bucket->remove($k)), а не видаляйте ім’я. Зміни спільні за посиланням: кожен, хто тримає той самий ключ, бачить їх одразу.
remove — це керування простором імен, а не знищення об’єкта
remove($key) скидає прив’язку та закріплення. Сам запис існує, доки на нього посилається будь-який інший дескриптор (захоплена змінна бутстрапу, значення, вкладене в інший Shared\Map, незавершений oxphp_async). Коли зникає останній дескриптор, запис, як звичайно, сам знімається з реєстрації.
Після remove ключ вільний. Наступний Registry::map($key, …) створює новий запис з іншим id.
Захоплені дескриптори попередньої прив’язки й далі працюють зі старим (тепер анонімним) записом; вони не переходять автоматично на новий.
$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Якщо ви ротуєте ключі (записи для окремих тенантів, що з’являються й зникають, версіонування ключів), звертайтеся до них за іменем під час кожного виклику (Registry::map($key, …) у кожному запиті), а не захоплюйте дескриптор один раз у бутстрапі. Захоплені дескриптори разом із ротацією ключів непомітно розходяться; звернення за іменем завжди сходиться до поточної прив’язки.
remove повертає true, якщо прив’язаний ключ було скинуто, і false, якщо ключа не було.
Помилки
| Виняток | Коли |
|---|---|
Shared\TypeException |
Типізований метод викликано для ключа, прив’язаного до іншого типу; фабрика повернула не той тип Shared\* або не Shareable. |
Shared\CapacityException |
Створення перевищило б ліміти SHARED_MAX_ENTRIES / SHARED_MAX_BYTES. |
Shared\DeadlockException (повторний вхід) |
Registry::map($key, …) для того самого $key зсередини його власної фабрики в тому самому потоці. |
Shared\DeadlockException (цикл між ключами) |
Очікування на слот Creating іншого потоку тривало понад 30 с — найімовірніше, фабрика A тримає ключ K1 і чекає на K2, фабрику якого тримає потік B, що чекає на K1. Повідомлення відрізняється від випадку повторного входу. |
Shared\SharedException (завершення роботи) |
Сервер зупиняється — реєстр відмовляє в нових отриманнях і прив’язках. Очікувано під час коректного завершення; це не помилка в коді. |
Shared\SharedException (перегони прив’язки) |
Інший творець уже зайняв слот, поки виконувалася фабрика цього потоку (запис фабрики НЕ було закріплено під ключем). Повторіть виклик. |
\InvalidArgumentException (SPL) |
Порожній $key. Перевірка аргументів, окремо від доменних помилок типу. |
| (виняток фабрики) | Якщо фабрика кидає виняток, слот скасовується (Creating → відсутній, ті, хто чекав, прокидаються для повторної спроби), а початковий виняток передається творцю. Фатальна помилка всередині фабрики так само скасовує слот і все одно завершує запит. |
Shared\DeadlockException розширює OxPHP\Async\AsyncException, тож catch (AsyncException) перехоплює його разом із тайм-аутами обмеженого очікування в інших місцях Shared\*. Обидва випадки DeadlockException мають спільний клас; розрізняйте їх за повідомленням ("reentrant get-or-create" проти "waited too long … cross-key cycle").
Використання ресурсів та інтроспекція
Registry::memoryUsage() і Registry::count() звітують про весь шар Shared*, а не лише про іменовані записи. Анонімні записи, створені через new Shared\*() (основна частина нинішнього використання Shared\*: захоплення в бутстрапі, значення в дорозі всередині Map і Channel, захоплення в асинхронних файберах), також враховуються.
Це зроблено навмисно. Обидва числа існують для моніторингу ємності та OOM; такий моніторинг має бачити анонімний стан окремих воркерів і незавершених операцій, а не лише іменований простір. Як наслідок:
- Обидва числа мінливі: вони зростають і спадають разом із поточними запитами та дескрипторами воркерів.
Registry::count()не дорівнюєcount(Registry::keys()).keys()— це лише іменований простір.memoryUsage()— статична облікова оцінка, а не справжній RSS. Це те саме число, з яким порівнюєтьсяSHARED_MAX_BYTESпід час створення Shared-об’єкта, і воно може перевищувати цей ліміт. Щоб дізнатися реальний обсяг купи, скористайтеся профілювальником купи (heaptrack,jemalloc_stats_print,mi_stats_print) або метриками пам’яті контейнера.
Деталі по кожному запису (id, тип, лічильник посилань, вартість у байтах) доступні на внутрішньому ендпоінті інтроспекції за адресою /__ox_shared/entries. PHP API для окремих записів навмисно немає, щоб не дублювати цей інтерфейс.
Коли не варто використовувати
- Між процесами та хостами. Реєстр живе всередині одного процесу OxPHP. Кілька екземплярів OxPHP його не поділяють. Використовуйте Redis / NATS / ваш наявний брокер; див. Перехід на зовнішнє сховище.
- Збереження після перезапусків. Реєстр зникає, коли процес завершується. Зберігайте дані в тому ж зовнішньому сховищі.
- Ефемерні ключі з високою плинністю. Через закріплення за замовчуванням динамічні ключі, які ви генеруєте в кожному запиті, лишають записи, доки ви не викличете
remove. ЛімітиSHARED_MAX_*це обмежують, але так робити все одно погано. Для короткочасного стану в межах запиту використовуйте звичайну змінну PHP. - Як примітив інвалідації кешу.
remove($key)призначений для виведення імені з ужитку, а не для «очищення кешу». Інвалідуйте вміст на місці ($map->clear(),$map->remove($member_key)); прив’язка імені зберігається.
Промпти для вашого агента
Ці промпти розраховані на агента з підключеним MCP-сервером документації OxPHP — потрібні сторінки він прочитає сам.
Проведи аудит дескрипторів Shared\*, які створюються напряму всередині бутстрапу воркера, — так виходить окремий екземпляр на кожен потік воркера, а не один спільний запис. Прочитай сторінку `shared-state/shared-registry` через MCP-сервер документації OxPHP.
Знайди їх у цьому репозиторії й покажи для кожного виклик `Registry::`, який дає йому ім’я, щоб усі воркери сходилися до того самого запису.Дивіться також
- Спільний стан. Огляд шару, ідентичності за дескриптором і того, коли
new Shared\*()— правильний інструмент. - Shared\Map, Shared\Counter, Shared\Pool та інші типізовані примітиви, які повертає
Registry. - Спостережуваність Shared. JSON API
/__ox_shared/*та метрики Prometheus. - Перехід на зовнішнє сховище. Коли одного процесу вже замало.