Shared\Registry
OxPHP\Shared\Registry は、OxPHP\Shared\* の他の部分を補完する名前をキーとした存在です。new Shared\Map() がハンドルの伝播(use キャプチャ、非同期ファイバー、ネスト)によってのみ共有される匿名エントリを生成するのに対して、Registry::map('cache', fn() => new Shared\Map(...)) はエントリを文字列キーのもとにバインドします。任意のワーカースレッド上、任意のリクエスト内で Registry::map('cache', …) を呼び出すすべての呼び出し元が、同じエントリを取得します。
これはひとつの問いに答えます。「1 つの Shared\Map をすべてのワーカー間で、あるいは従来モードのすべてのリクエスト間で、どうやって共有するのか?」 です。可変状態の適切な単位は依然として他の Shared\* 型です。Registry は、そのうちの 1 つに名前を付けるための手段にすぎません。
メンタルモデル
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)を最初に呼び出した呼び出し元が、ファクトリを実行し、生成されたエントリを名前のもとにピン留めします。 - それ以降のすべての呼び出し元(同じスレッド、他のワーカー、後続のリクエスト)は、同じエントリを受け取ります。ファクトリは再実行されません。ヒット時には無視されます。
- 並行して最初にアクセスした場合は、キーごとのゲートでブロックされます。ちょうど1 つのスレッドだけがファクトリを実行し、他のスレッドは待機して勝者のエントリを受け取ります。これにより、コネクションプールでのリソースの二重取得が防がれます。
名前による同一性は、ハンドルによる同一性を補完します。匿名エントリ(new Shared\*())と名前付きエントリは、同じプロセスグローバルなレジストリ内で共存します。名前インデックスは、その上に文字列ルックアップを追加するものです。
クイックスタート — すべてのワーカーにまたがる 1 つのカウンター
<?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())と比較してみましょう。そのパターンはワーカースレッドごとに1 つのカウンターを生成します。各ワーカーはそれぞれ独自のブートストラップを実行し、それぞれ独自の匿名エントリを取得します。集計カウントは、ワーカープールのサイズ分の倍数だけ食い違います。一方 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(再入) |
同じスレッド上で、あるファクトリの内部から同じ $key に対して Registry::map($key, …) を呼び出した場合。 |
Shared\DeadlockException(クロスキーの循環) |
別のスレッドの Creating スロットで 30 秒を超えて待機した場合。最も可能性が高いのは、ファクトリ A がキー K1 を保持したまま K2 を待機し、その K2 のファクトリを保持しているスレッド B が K1 を待機している、というケースです。再入のケースとは別のメッセージになります。 |
Shared\SharedException(ドレイン中) |
サーバーがシャットダウン中の場合。レジストリは新しい取得とバインドを拒否します。グレースフルシャットダウン中に想定される動作であり、コードのバグではありません。 |
Shared\SharedException(バインドの競合) |
このスレッドのファクトリが実行されている間に、別の作成者がすでにスロットに確定していた場合(そのファクトリのエントリはキーのもとにピン留めされませんでした)。呼び出しをリトライしてください。 |
\InvalidArgumentException(SPL) |
$key が空の場合。ドメインの型エラーとは別の、引数の検証です。 |
| (ファクトリの例外) | ファクトリがスローした場合、スロットは中止され(Creating → 不在、待機側はリトライのために起床します)、元の例外が作成者へ伝播します。 |
Shared\DeadlockException は OxPHP\Async\AsyncException を継承しているため、catch (AsyncException) は、これを Shared\* の他の箇所にある有限待機のタイムアウトとまとめて捕捉します。2 つの異なる DeadlockException のケースはクラスを共有しています。メッセージ("reentrant get-or-create" と "waited too long … cross-key cycle")で見分けてください。
メモリとイントロスペクション
Registry::memoryUsage() と Registry::count() は、名前付きエントリだけでなく、Shared* レイヤー全体を報告します。new Shared\*() で作成された匿名エントリ(現在の Shared\* の使用の大半を占めます。ブートストラップでのキャプチャ、Map や Channel の中の処理中の値、非同期ファイバーでのキャプチャなど)も含まれます。
これは意図的なものです。この 2 つの数値は、キャパシティ / OOM の監視のために存在します。その監視は、名前付きの名前空間だけでなく、ワーカーごとの状態や処理中の匿名状態も把握しなければなりません。その結果:
- どちらの数値も一時的です。処理中のリクエストやワーカーごとのハンドルに応じて増減します。
Registry::count()はcount(Registry::keys())と等しくありません。keys()は名前付きの名前空間のみを対象とします。memoryUsage()は、実際の RSS ではなく、静的な会計上の推定値です。これはSHARED_MAX_BYTESが上限を設ける数値と同じものです。実際のヒープのフットプリントについては、ヒーププロファイラー(heaptrack、jemalloc_stats_print、mi_stats_print)またはコンテナのメモリメトリクスを使用してください。
エントリごとの詳細(id、型、参照カウント、バイトコスト)は、/__ox_shared/entries にある内部イントロスペクションエンドポイントにあります。そのインターフェースを重複させないよう、エントリごとの PHP API は意図的に用意されていません。
使用すべきでない場合
- プロセスをまたぐ、ホストをまたぐ場合。 レジストリは 1 つの OxPHP プロセスの内部に存在します。複数の OxPHP インスタンス間では共有されません。Redis / NATS / 既存のブローカーを使用してください。外部ストアへの移行を参照してください。
- 再起動をまたいだ永続性。 レジストリはプロセスの終了時に消え去ります。同じ外部ストアを介して永続化してください。
- 入れ替わりの激しい一時的なキー。 デフォルトでピン留めするというセマンティクスは、リクエストごとに生成する動的なキーが、
removeを呼び出すまでエントリをリークすることを意味します。SHARED_MAX_*の上限で制限はされますが、それでも良くない使い方です。リクエストごとの短命な状態には、通常の PHP 変数を使用してください。 - キャッシュ無効化のプリミティブとして。
remove($key)は名前を廃止するためのものであり、「キャッシュをクリアする」ためのものではありません。中身はその場で無効化し($map->clear()、$map->remove($member_key))、名前のバインディングは残してください。
関連項目
- 共有状態。レイヤーの概要、ハンドルによる同一性、そして
new Shared\*()が適切なツールとなる場面について。 - Shared\Map、Shared\Counter、Shared\Pool、および
Registryが返すその他の型付きプリミティブ。 - 共有オブザーバビリティ。
/__ox_shared/*の JSON API と Prometheus メトリクス。 - 外部ストアへの移行。1 つのプロセスの限界を超えたとき。