Shared\Map

OxPHP\Shared\Map は共有レジストリ内に存在し、プロセス内のすべての PHP ワーカーから見える並行マップです。2 つのワーカー、あるいはリクエストハンドラーとバックグラウンドタスクが、リクエストのライフサイクルをまたいで存続する可変状態を共有する必要があるときに、まず選ぶべきプリミティブです。

概要

  • int|string → mixed キーは PHP の整数または文字列で、区別されて保持されます(123"123" は別のキーであり、PHP の配列のようなキーの型強制は行われません)。文字列キーはバイナリセーフで、不透明なバイト列として(PHP の配列 / Go / Redis のように)保存されるため、非 UTF-8 のキー(NUL の埋め込みを含む)も忠実にラウンドトリップします。値には任意のスカラー、スカラー/配列からなる配列、あるいは別の Shareable インスタンスを指定できます。
  • null は不在を意味し、格納された値として扱われることは決してありません。 null 値を書き込むと TypeException がスローされます。null が返された場合は常に「そのようなキーは存在しない」ことを意味します。これにより、get() が null を返す古典的な曖昧さを根本から取り除きます(java.util.concurrent.ConcurrentHashMap や Go の sync.Map と同じ選択です)。
  • 単一の線形化可能な条件付きプリミティブ。 compareAndSetnull という不在センチネルを介してアトミックな挿入 / 置換 / 削除をカバーします。あらゆる read-modify-write はこの上に構築します。
  • 並行。 異なるワーカーからの書き込みに外部ロックは不要です。キーごとの操作はシャードレベルでアトミックです。
  • 循環に対して安全。 この Map へと戻って到達するような Shareable を格納しようとすると、いかなる変更が行われる前に CycleException で拒否されます。拒否された経路でリークは発生しません。
  • おおよそのソフト上限で制限されます。 maxEntries は OOM 対策の上限であり、正確なカウントではありません。

API リファレンス

php
namespace OxPHP\Shared; final class Map implements Shareable { public function __construct(?int $maxEntries = null); // null = unbounded; <= 0 throws TypeException // reads public function get(int|string $key): mixed; // null ⟺ absent public function getMany(iterable $keys): \Iterator; // lazy; skips absent keys public function count(): int; // striped, weakly consistent public function maxEntries(): ?int; // writes public function set(int|string $key, mixed $value): void; public function setIfAbsent(int|string $key, mixed $value): mixed; // prev; null ⟺ inserted public function setMany(iterable $entries): int; public function remove(int|string $key): bool; // existed? public function removeMany(iterable $keys): int; public function clear(): int; // entries removed // value-returning public function swap(int|string $key, mixed $value): mixed; // prev; null ⟺ was absent public function pop(int|string $key): mixed; // prev; null ⟺ was absent // conditional (single linearisable primitive) public function compareAndSet(int|string $key, mixed $expected, mixed $new): bool; // iteration public function forEach(callable $fn): void; // weakly consistent; callback runs lock-free public function id(): int; }
メソッド ユースケース
__construct オプションの maxEntries 上限付きで生成します(null = 無制限、<= 0 はスロー)。
get キーで取得します。null ⟺ 不在。
getMany 既知のキーについて key => value を遅延ストリームします。不在のキーはスキップされます(後述)。
count おおよそのエントリ数(並行書き込み下では弱一貫性)。
maxEntries 設定された上限を報告します(無制限の場合は null)。
set 挿入または置換します。以前の値はマテリアライズされません。
setIfAbsent 存在しなければアトミックに挿入します。既存の値を返し、挿入した場合は null を返します。
setMany 任意の iterable から一括挿入します。書き込んだ件数を返します。
remove キーを削除します。存在していたかどうかを返します(値はマテリアライズされません)。
removeMany 一括削除します。実際に削除された件数を返します。
clear すべてのエントリを破棄します(ネストされた Shareable の保持を解放します)。削除した件数を返します。
swap 上書きして以前の値を返します(null ⟺ 不在だった)。
pop 削除して以前の値を返します(null ⟺ 不在だった)。
compareAndSet 現在の内容を条件にアトミックな挿入 / 置換 / 削除を行います(後述)。
forEach 弱一貫性のトラバース。コールバックはロックを保持せずに実行されます。
id 数値のレジストリ識別子。ロギングや /__ox_shared/entry?id=… に便利です。

has()update()getOrSet()keys()trySet()updateMany() は存在せず、このクラスは Countable実装していません旧来の API からの移行を参照してください。

null を不在とするモデル

null はあらゆる場所で不在センチネルとして予約されています。

  • null 値を伴う set / swap / setIfAbsentTypeException
  • null を返す get / swap / pop / setIfAbsent ⟺ キーが不在だった。
  • compareAndSet では、どちらの側の null も「不在」を意味します(「null を格納する」ではありません)。

「値なし」を記録する必要がある場合は、null を格納するのではなく、キーを削除してください(またはキーの不在を利用してください)。has() が存在せず、has()+get() の並行レースも存在しないため、存在確認は単一の get($k) !== null でアトミックに行えます。

compareAndSet — 条件付きプリミティブ

php
$map->compareAndSet($key, expected: null, new: $v); // insert iff absent (= setIfAbsent, returns bool) $map->compareAndSet($key, expected: $a, new: $b); // replace iff current === $a $map->compareAndSet($key, expected: $a, new: null); // remove iff current === $a

スワップが適用された場合にのみ true を返します。等価性は内容による判定です。スカラーは値で、文字列と配列はシリアライズされたバイト列で、ネストされた Shareable 値はレジストリの同一性で比較されます。配列の等価性は、一般的なケース(リスト、すべて int キーまたはすべて string キーの配列)では PHP の === と一致します。int キーと string キーが混在する配列は Map の正規化された格納形式で比較されるため、特定の int/string の並び順は区別されません(Map は読み戻し時にもそうした配列を並べ替えます)。read-modify-write は明示的なリトライループとして構築してください。競合下では複数回実行されるため、クロージャは純粋に保ってください。

php
do { $cur = $map->get('counter'); // null if absent $next = ($cur ?? 0) + 1; } while (!$map->compareAndSet('counter', $cur, $next));

ここに ABA 問題はありません。ストアはコンテンツアドレス方式であり(値ストアにおいて、内容が等しい値は同じ値です)、ネストされた Shareable の同一性は単調増加し再利用されないレジストリ ID を使用します。スタンピードに対して安全な遅延初期化には Shared\Once を、プールされたリソースには Shared\Pool を使用してください。

メモリモデル — コピーが発生する場所

値は zval としてではなく、シリアライズされた表現として格納されます。そのため「ゼロコピー」は値には当てはまりません。

操作 共有ヒープへのシリアライズ 以前の値を zval へマテリアライズ
set / setMany はい いいえ
remove / removeMany いいえ
setIfAbsent はい 以前の値が存在する場合のみ
swap / pop はい / — はい
get / getMany (キーのみ) はい
compareAndSet はい($new いいえ

書き込みパスでのシリアライズは、共有メモリに入るあらゆる値にとって避けられません。新しい zval への読み戻しのコストは、以前の値/参照した値を返すメソッドだけが支払います。したがって set/remove は「戻り値のマテリアライズがない」のであって、「無料」ではありません。例外はネストされた Shareable 値です。これは(id と参照カウントのインクリメントとして)参照で格納され、ディープコピーされません。

並行処理

  • count() は弱一貫性です。 エントリ数はシャードごとにストライプ化され、読み取り時に合計されます。結果はマップが静止しているときは正確で、並行書き込み下では近い近似値になります(ConcurrentHashMap::size と同じ契約です)。ストライピングにより、書き込みが単一のホットなカウンターに集中しないようにします。
  • maxEntries はソフト上限です。 これはストライプ化された合計に対してチェックされるため、並行挿入下では、CapacityException で新しいキーを拒否する前に、マップがシャード数の分だけ超過することがあります。正確なカウントではなく、OOM 対策の予算として扱ってください。既存のキーの上書きは、上限に達していても常に成功します。エビクションはありません — LRU/TTL エビクションを備えたキャッシュは別のプリミティブです。
  • forEach はロックを保持せずにコールバックを実行します。 一度に 1 つのシャードのキーをスナップショットし、シャードを解放してから、各値を再取得して $fn(key, value) を呼び出します。スナップショットと呼び出しの間に削除されたキーはスキップされます。シャードのスナップショット後に追加されたキーは見落とされる可能性があります。値はスナップショット時点よりも新しい場合があります。早期に停止するにはコールバックから false を返します。キーのみがスナップショットされるため、遅いコールバックが削除された値を固定し続けることはありません。

共有設定キャッシュ

php
<?php $config = new OxPHP\Shared\Map(maxEntries: 1024); // Warm once at app bootstrap. $config->setMany([ 'rate_limit.default_rpm' => 600, 'feature.new_checkout' => true, 'timeout.downstream_ms' => 250, ]); // Any request handler reads without contention; null ⟺ not configured. $rpm = $config->get('rate_limit.default_rpm') ?? 60;

テナントごとのレート制限

php
<?php $buckets = new OxPHP\Shared\Map(maxEntries: 50_000); $key = "tenant:{$tenantId}"; $prev = $buckets->setIfAbsent($key, ['tokens' => 100, 'refill_at' => time() + 60]); // $prev === null ⟺ we created the bucket; otherwise it holds the existing one. $state = $buckets->get($key); if ($state['tokens'] === 0) { throw new RateLimitException(); }

ワーカー間でのカウンターの調整

php
<?php $counters = new OxPHP\Shared\Map(); $counters->set('requests_handled', new OxPHP\Shared\Counter()); // Any worker increments via the stored Shareable (stored by reference). $counters->get('requests_handled')->add();

大きなマップのイテレーション

php
<?php $sessions->forEach(function (int|string $key, mixed $value): bool|null { if ($value['expires_at'] < time()) { // safe: forEach holds no lock during the callback return null; // keep going } return null; }); // Or read a known subset lazily, stopping early: foreach ($cache->getMany($hotKeys) as $key => $value) { if (enoughCollected()) break; // remaining keys are never materialised handle($key, $value); }

セマンティクスと注意点

配列は読み取り時にコピーされる

php
<?php $m = new OxPHP\Shared\Map(); $m->set('cfg', ['timeout' => 5, 'retries' => 3]); $cfg = $m->get('cfg'); $cfg['timeout'] = 10; // mutates the returned copy only // $m->get('cfg')['timeout'] is still 5

配列値をアトミックに更新するには、それを読み取り、コピーを変更し、compareAndSet でコミットします(競合時はリトライします)。あるいは、独立して変化するフィールドをネストされた Shared\Counter / Shared\Map として格納します。

ネストされた Shareable の retain は自動

php
<?php $map = new OxPHP\Shared\Map(); $counter = new OxPHP\Shared\Counter(10); $map->set('c', $counter); $retrieved = $map->get('c'); // same Shareable identity $retrieved->add(); // mutation visible via $counter too echo $counter->get(); // 11 $counter2 = $map->pop('c'); // Map releases its hold, returns the value $counter2->add(); // still alive via the returned wrapper

循環検出は変更前に拒否する

php
<?php $a = new OxPHP\Shared\Map(); $b = new OxPHP\Shared\Map(); $a->set('b', $b); // fine try { $b->set('a', $a); // closes the loop } catch (OxPHP\Shared\CycleException $e) { // message: "cycle would form: #… → #… (inserting into #…)" } $b->get('a'); // null — $b untouched, no leaked retains

配列内のネストされた参照もチェックされます。ウォーカーは SHARED_CYCLE_DETECT_DEPTH(デフォルト 16)と SHARED_CYCLE_DETECT_EDGES(デフォルト 10 000)で制限されます。非常に大きなグラフでは bounds exceeded を伴う CycleException が発生します。環境変数のつまみを引き上げるか、グラフを分割してください。

値ごとのサイズ上限

Note

シリアライズ後のサイズが SHARED_MAX_VALUE_SIZE(デフォルト 1 MiB)を超える単一の値は、ValueTooLargeException で拒否されます。これは PHP 側の入力によるアロケーション爆弾を防ぎます。すべての書き込みパス(setsetIfAbsentswapcompareAndSetsetMany)に適用されます。

バッチ操作はキーごとにアトミックであり、バッチ単位ではアトミックではない

Warning

setManygetManyremoveMany は一度に 1 つのキーを処理します。setMany が途中で CapacityExceptionCycleExceptionValueTooLargeException に遭遇した場合、それ以前のキーは格納されたまま残ります。この部分的な成功は意図的なものです。オールオアナッシングのセマンティクスが必要な場合は、マップの周りで Shared\Mutex を使用してください。

旧来の API からの移行

破壊的変更

これは互換性シムのない破壊的な再設計です。

has($k) get($k) !== null(アトミック — has/get レースなし)
get($k, $default) get($k) ?? $default
trySet($k, $v): bool setIfAbsent($k, $v): mixed(以前の値を返す。null ⟺ 挿入された)
remove($k)(以前の値を返していた) remove($k): bool、または値を取得するには pop($k)
update($k, $fn) compareAndSet のリトライループ、または一度きりの初期化には Shared\Once
getOrSet($k, $fn) setIfAbsent、またはケースに応じて Shared\Once / Shared\Pool
updateMany(...) compareAndSet のループ
keys(): array forEach(...)、または getMany($knownKeys)
count($map)(Countable) $map->count()
null 値の格納 キーの不在 / remove を使用

例外

失敗しうるすべてのメソッドは OxPHP\Shared\SharedException のサブクラスをスローします。

例外 発生元
CapacityException maxEntries を超える新しいキー(set / setIfAbsent / compareAndSet / setMany)。
ValueTooLargeException 値ごとの上限(SHARED_MAX_VALUE_SIZE)を超える値。
CycleException 到達可能性の循環を閉じてしまう書き込み(extends TypeException)。
TypeException null 値、格納できない値(object/closure/resource)、int/string でないキー、maxEntries <= 0
StaleHandleException レジストリエントリがエビクトされたハンドルに対するメソッド呼び出し。

可観測性

すべての Map は内部 API を通じて可視化されます。

  • GET /__ox_shared/summaryMap を含む、型ごとの集計数。
  • GET /__ox_shared/entries — id / type / refcount / mem_bytes を含むすべてのエントリの一覧。
  • GET /__ox_shared/entry?id=N — Map のインスタンスごとの詳細には key_countmax_entriessaturationsample_keys(プレビュー上限で切り詰められます)が含まれます。
  • GET /__ox_shared/graph?id=N[&depth=D][&edges=E] — 発信 Shareable 参照の BFS 走査。CycleException の後に便利です。

Prometheus は /metrics で Map ごとのゲージを公開します。

メトリクス 意味
oxphp_shared_map_entries{map_id="…"} 現在の(おおよその)キー数。
oxphp_shared_map_max_entries{map_id="…"} 設定された上限(無制限の場合は 0)。
oxphp_shared_map_saturation{map_id="…"} entries / max_entries、無制限の場合は 0。

設定

環境変数 デフォルト 効果
SHARED_MAX_ENTRIES 100 000 すべての Shared エントリを合わせたグローバル上限。
SHARED_MAX_BYTES 1 GiB すべての Shared エントリにわたる推定メモリのグローバル上限。
SHARED_MAX_VALUE_SIZE 1 MiB 値ごとのシリアライズ後サイズ上限。これより大きい値は ValueTooLargeException をスローします。
SHARED_CYCLE_DETECT_DEPTH 16 循環チェック時の最大 BFS 深度。深い正当なグラフの場合は引き上げます。
SHARED_CYCLE_DETECT_EDGES 10 000 循環チェック時に走査する最大エッジ数。密な正当なグラフの場合は引き上げます。
SHARED_PREVIEW_ARRAY_LIMIT 20 /entry?id=…sample_keys でサンプリングされるエントリ数。
SHARED_INTROSPECTION_ENABLED true /__ox_shared/* API を切り替えます。

関連

  • Shared\Counter — アトミックな整数。キーごとのヒット数のために Map 内に格納します。
  • Shared\OncesetIfAbsent では高価なファクトリが再実行されてしまう場合の、スタンピードに対して安全な遅延初期化。
  • Shared\Channel — MPMC キュー。キーによる検索ではなく FIFO パイプラインが必要なときに補完的です。
  • Shared\Mutex — 格納された値の周りで厳密な相互排他が必要な場合。