Shared\Once

OxPHP\Shared\Once は、初期化クロージャを 単一の Once セル に対してちょうど一度だけ実行し、その結果を同じセルの以降のすべての呼び出し元に見えるようにします。「高コストな処理を最大でも一度だけ実行したい」という場面のためのプリミティブです。

ワーカーをまたぎ、リクエストをまたいだ真の「プロセス全体でちょうど一度」というセマンティクスを得るには、Shared\Registry::once(...) を介して Once を名前にバインドし、すべてのワーカーが同じセルに収束するようにします。素の new Shared\Once() コンストラクタパターンは、ワーカーモードではワーカースレッドごとに別々のセルを生成し(各ワーカーのブートストラップがコンストラクタを実行するため)、従来モードではリクエストごとに別々のセルを生成します。これはセルごとに一度であって、プロセスごとに一度ではありません。

概要

  • ワーカーをまたいだ 単一セルに対する 一度だけの実行。 同じ Once セルに対して 2 つのワーカーが getOrInit($factory) に競って入ると、ファクトリはそのうち片方だけで実行されます。負けた側はブロックされ、勝った側の値を受け取ります。Shared\Registry::once と組み合わせることで、「同じセル」を「すべてのワーカーで同じ名前」という意味にできます。
  • 4 状態のステートマシン。 セルは UninitializedPending(今まさにファクトリが実行中)、ReadyPoisoned のいずれかです。status() で読み取ります。
  • あいまいな null がない。 get() は未設定のセルに対して null を返す代わりに例外を投げます。したがって、格納された null は「欠落」ではなく本物の値です。
  • 再入安全。 ファクトリ自身の内部から同じ Once に対して getOrInit() を呼び出すと、ハングする代わりに 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 勝者か? 既に手元にある値のプッシュモデル初期化(副作用のあるリソースなし)。
getOrInit 格納された値 プルモデル初期化。競合のない標準的なプリミティブ。
id レジストリ 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() はキャッシュスタンピードに強いパターンです。同時に初回アクセスが殺到しても、ファクトリは 成功したときに ちょうど一度だけ実行され、競合に負けた呼び出し元を含むすべての呼び出し元が勝者の値を受け取ります。勝ったファクトリが 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() はイントロスペクションのためのものです。ファクトリをトリガーすることは決してなく、poison されたセルであっても例外を投げません。実際に値を競合なく取得するには、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 を参照してください。Once1 つ の値を、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());

セマンティクスと落とし穴

  • セルが Ready でないとき get() は例外を投げます。 空または Pending なセルには UninitializedException、poison されたセルには PoisonedException です。例外なしで分岐するには status() を、安全に値を取得するには getOrInit() を使用します。
  • ファクトリは初期化の成功ごとに最大でも一度だけ実行されます。 同時に呼び出した側は勝者に対してブロックされ、自分のコピーを実行することはありません。
  • 失敗ポリシーは呼び出しごとではなく構築時に設定します。 Reset(デフォルト)はファクトリの失敗時にセルを Uninitialized に戻し、後続の呼び出しがリトライできるようにします。Poison はセルを終端的に Poisoned にします。どちらのモードでも、ファクトリの例外は 現在の 呼び出し元へ再スローされます。
  • 完全な値の範囲。 スカラー、配列、およびネストされた Shareable 値は格納して読み戻せます。クロージャ、リソース、および Shareable でない PHP オブジェクトは TypeException を発生させます。
再入は例外を投げる

ファクトリ自身の内部からの getOrInit()DeadlockException を発生させます。内側の呼び出しが別の Once を使うように構造を組み直してください。

Poison はスレッドをまたいで誠実だが、オブジェクトとして同一ではない

PHP の例外オブジェクトはワーカースレッドをまたげないため、poison されたセルは失敗のクラス、メッセージ、コードをキャプチャします。以降の呼び出し元は、どのスレッドからでもその情報を持った新しい PoisonedException を受け取ります。同じ詳細ではありますが、同じオブジェクトではありません。

例外

例外 発生元
UninitializedException Uninitialized または Pending なセルに対する get()
PoisonedException Poisoned なセルに対する get() / getOrInit() / trySet()
DeadlockException ファクトリから同じ Once に対して再帰的に呼ばれた getOrInit()
TypeException 格納された値がシリアライズ不可能(クロージャ、リソース)。
StaleHandleException レジストリエントリが退避されたハンドルに対する任意のメソッド。

ファクトリ自身が例外を投げた場合、その例外はそのまま現在の呼び出し元へ伝播します。Reset モードではセルは未初期化のままとなり、次の getOrInit がリトライします。Poison モードではセルは poison されます。

オブザーバビリティ

Shared Observability を参照してください。クイックリファレンス:

  • GET /__ox_shared/entry?id=N{ status: "uninitialized" | "pending" | "ready" | "poisoned", type: "Once" } に加えて、ready の場合は格納された値のプレビューを公開します。

使うべきでない場合

  • 作成後に変化する値。 Once は一度だけ書き込むものです。格納された状態が変化する場合は Shared\Mutex または Shared\Map を使用してください。
  • ワーカーローカルな状態。 値を共有する必要がない場合は、静的クラスプロパティやモジュールグローバルの方が安価です。
  • 高コストな リクエストごとの 計算。 共有状態ではなくリクエスト内でキャッシュしてください。そうしないとメモリをリークします。

関連

  • Shared State — 概要とメンタルモデル。
  • Shared\Mutex — 一度きりの値が後で変化する場合。
  • Shared\PoolN 個の等価なリソースの一度きりの初期化。
  • Shared\MapgetOrSet($key, $factory) を使ったキー付き初期化。