コンテンツにスキップ

ADR-15 — アナライザーのRactorベース並行性モデル

ステータス: Accepted, 2026-05-14;フォークベースのバックエンドがアクティブな並行バックエンド(修正2026-05-20)

フェーズ1 / 2a / 2b / 3a / 4a / 4b / 4b.x / 4cはすべてランド済み。Ractorプール(フェーズ4b/4cの配線)はRuby Bug #22075によりブロック中;workers > 0の出荷バックエンドはフォークプール。フェーズ3bとOQ1 / OQ2は需要駆動。

RigorのアナライザーはCPUバウンドのRubyです。v0.1.4時点でのウォームキャッシュrigor check libプロファイル(157ファイル、stackprof wall(1000)、1340サンプル)の内訳は:

フェーズwall割合注記
推論(ExpressionTyper#type_ofMethodDispatcher.dispatch_precise_tiers約50%アナライザーの主な仕事
Marshal.load(起動時のキャッシュヒット)22.5%RBS env / instance_defs / singleton_defsのデシリアライズ
GC(mark + sweep)約15%Ruby標準のオーバーヘッド
Prismパース約3%すでに高速
その他(ファイルウォーク、CheckRules編成、プラグインフック)約9%ファイルごとの儀式

2つのインフラストラクチャ変更がすでに着地:

  • Cache::Storeスレッドセーフティ(コミット31e95c8) — 再入可能なMonitor@memoプラスヒット / ミス / ライトカウンタをガードするため、並行ワーカーが1つのStoreを競合なしで共有できる。
  • Cache::Storeインプロセスメモ(コミット5c30b37) — インメモリレイヤーが繰り返しの(producer_id, key)キャッシュヒットをプロセスあたり1つのMarshal.loadに畳み込む。RSpec側の勝利は6×(162秒 → 27秒、parallel_tests使用);シングルショットCLIではレイヤーは休眠中、各プロデューサーが1回しか参照されないため。

マルチコア利用はThreadベースのファイル並列性(Runner#analyze_filesがワーカープール経由でディスパッチ)でプロトタイプ化された。結果はネガティブ: RIGOR_WORKERS=4で12コアマシン上のwall-clockが1.85秒から2.15秒に増えた。RubyのGVLがCPUバウンド作業をシリアライズし、アナライザーは圧倒的にCPUバウンド;スレッド協調のオーバーヘッドが(ゼロの)GVLリリースによるゲインを上回った。コードは差し戻された;発見はdocs/CURRENT_WORK.mdオープン項目#7に記録されている。

MRI Ruby 4.xでマルチコア利用への唯一の実現可能なパスは:

  1. フォークベースのワーカー — 親に協調された独立プロセス。GVLを完全にバイパス。プロセスごとの起動コスト(macOSで約50〜100ms)プラスプロセスごとのEnvironment再構築が高速化エンベロープを制限する;数百ファイルまたは複数秒の解析テールを持つプロジェクトでのみ正味プラスになる。
  2. Ractor — GVLをバイパスしながらインプロセスメモリセマンティクスを保つ、共有しない並行プリミティブ。Ruby 3.x+とRuby 4.xで安定。厳格な共有可能性制約(Ractor境界を越えるすべてのオブジェクトはRactor.shareable?でなければならない(MUST))がアナライザー全体の採用を自明でなくする。

ADR-15はRactorを主要な並行性の方向性としてコミットします。フォークベースのワーカーは除外されていない — 大きなプロジェクト向けの素早い勝利として最初に着地できる — が、このADRが固定する長期的な形はRactorベースです。

  • 利用可能なCPUコアでスケールするウォームキャッシュrigor checkでのwall-clock並列性。目標: 100+ファイルのプロジェクトは4ワーカーで3×以上の高速化;小さなプロジェクトは1.05×を超えない(ワーカー起動オーバーヘッドが償却される)。
  • 正確性のリグレッションなし。診断出力は完了順序に関係なく決定論的のまま。プラグイン契約(contract)は引き続き公開された動作を尊重する。
  • 段階的採用。各移行フェーズは独立して出荷され独立して取り消し可能。監査spec(spec/rigor/ractor_readiness_spec.rb)はフェーズ間の契約。
  • デーモン / ウォッチモード対応。Ractor分離ワーカーをサポートする形は、繰り返されるrun呼び出し(LSP、ウォッチモード、将来のrigor server)を呼び出しごとにRBS状態を再構築せずに処理する長寿命のAnalysis::Runnerインスタンスもサポートする。Cache::Storeインプロセスメモはこの半分を実装している;Environment分割が残りを完了させる。
  • デフォルトとしてのRactorベース実行の強制。フェーズ4はオプトイン(env var / configフラグ)として出荷;シーケンシャルパスがデフォルトのまま。Ractor実行は公開された例プラグインがすべてRactorテスト済みになり、ワーカープールの起動コストが低く検証されたときにデフォルトになる。
  • プロセスプール統合。フォークベースのワーカーはdocs/CURRENT_WORK.mdオープン項目#7で別途追跡。2つのパスは共存できる;このADRはフォークパスについていずれの方向にもコミットしない。
  • コードベース全体の純粋Ractorリファクタ。可変インプロセス状態は属する場所にとどまる(例: Ractorごとの推論キャッシュ)。分割は「凍結されたリフレクションレイヤー共有 + Ractorごとの可変キャッシュレイヤー」であり、「どこでもすべて不変」ではない。

Rigorはdocs/design/20260514-ractor-migration.mdに文書化された4フェーズ移行に従ったRactorベースの並行性を採用します:

  1. フェーズ1 — 値オブジェクトの共有可能性(着地済み)。エンジンがディスパッチを通じて送るすべてのリーフキャリア(carrier)は構築時にRactor.shareable?。カバレッジ: Type::*(16クラス)、TypeNode::*(7クラス)、Cache::DescriptorAnalysis::FactStoreFlowContribution。リグレッションガードはspec/rigor/ractor_readiness_spec.rb
  2. フェーズ2a — Configurationディープフリーズ(着地済み)Configuration#initialize@paths配列を凍結し、selffreezeを呼ぶ。2行の変更、動作シフトなし。
  3. フェーズ2b — Environment / RbsLoader分割(着地済み)。新しいRigor::Environment::Reflection値オブジェクトがローダーの読み取り専用RBSクエリサーフェス(surface)(5つの凍結テーブル + 祖先名)を保持し、純粋なHash / Setルックアップからclass_known? / instance_definition / singleton_definition / class_type_param_names / constant_type / class_orderingに応答する。RbsLoader#reflectionが1つを構築 + メモ化;新しいEnvironment#reflectionが委譲する。Reflectionはfrozen?だがRactor.shareable?ではない(下記WD6を参照)。各Ractorワーカー(フェーズ4)は共有されたCache::Storeから自身のReflectionを構築する;Reflection自体はRactor境界を越えない。
  4. フェーズ3 — プラグイン契約(フェーズ3a着地済み)Plugin::Blueprint(新規)は凍結されたRactor共有可能なリプレイディスクリプタで、klass_name + ディープフリーズされたconfigを運ぶ。Plugin::Registryは今、整列されたRactor共有可能なArray<Blueprint>としてblueprintsを公開し、Object.const_get + klass.new + #init(services)を介して各ブループリントをリプレイしてフレッシュなレジストリを構築するRegistry.materialize(blueprints:, services:)を追加する。プラグインgemはワーカーがスポーンする前にメインRactorからrequireされる — そのため、ワーカー内でブループリント解決がgemを再ロードせずに成功する。プラグインインスタンスは意図的に非共有のまま — ivar内の実行ごとの状態を蓄積する(rigor-sorbet@reachable_absurd_nodes / @reveal_type_calls / @assert_type_mismatchesが正典の例)。フェーズ4のワーカーパターンは: ブループリントを境界越しに送り、ワーカーごとに1度実体化し、インスタンスを共有しない。フェーズ3b(クロスRactorプラグイン集約状態 — § OQ2を参照)は、フェーズ4が実際の使用を測定するまで先送り。
  5. フェーズ4 — RactorワーカープールAnalysis::Runner#analyze_filesが凍結されたEnvironmentを共有するRactor.new割り当てプールにディスパッチする。結果の再アセンブリは元のパス順序を保持するため、診断ストリームは決定論的のまま。3つのサブフェーズに分解(完全なプランはdocs/design/20260514-ractor-migration.md § フェーズ4):
    • フェーズ4a(着地済み): Analysis::WorkerSession値キャリア。Ractor.shareable?入力(Configurationcache_storeArray<Plugin::Blueprint>)を取り、自身のプラグインサービス + 実体化されたレジストリ + DependencySourceInference::Index + Environment + セッションごとのRbsExtended::Reporter + BoundaryCrossReporterを構築するワーカーごとの基板。#analyze(path)Runner#analyze_fileの等価物。ループ内にまだRactorなし — 基板は存在し、ワーカーごとの所有権境界が分離してテスト可能になる。
    • フェーズ4b(着地済み): Runnerworkers: Nコンストラクタキーワードを取得(デフォルト0 = シーケンシャル)。N > 0のとき、ファイルごとの解析はWorkerSessionを実行するN個のRactorにディスパッチされる。ワーカーはRactor.main.send(Ruby 4.0+のメールボックスモデル — yieldは削除された)を介して書き戻す。コーディネーターはワーカーごとのレポーターをマージし、診断を元のパス順序で並べ替える。Environment::ClassRegistry.defaultRactor.make_shareableされ、ワーカーがRactor::IsolationErrorなしに読み取れる。CLIサーフェスは触れられないまま — workers:はこのスライス(slice)ではプログラム的なオプトインのみ。フェーズ4cがユーザー向けフラグを配線する。
    • フェーズ4c(着地済み): Configuration#parallel_workers(デフォルト0)が.rigor.ymlparallel.workers:を読む;CLIの--workers=NフラグとRIGOR_RACTOR_WORKERS env varがそれをオーバーライドする。優先順位: CLI > env > config > 0。デフォルトは引き続きシーケンシャル — プールモードは、ワーカー側のenv構築安定性作業(フェーズ4b.x;下記§「既知の制限」を参照)が着地するまでオプトインのまま。プールspecはRIGOR_INCLUDE_RACTOR_POOL=1の後ろにゲートされ、デフォルトのmake verifyが決定論的のまま;make test-ractor-poolが分離して実行する。

spec/rigor/ractor_readiness_spec.rbの監査specがフェーズ間の契約。マッチするRactor.shareable?アサーションを書かずに新しい値オブジェクトクラスを追加するのはリグレッション。

Amendment(2026-05-20) — フォークがアクティブな並列バックエンドになる

Section titled “Amendment(2026-05-20) — フォークがアクティブな並列バックエンドになる”

フェーズ4(Ractorワーカープール)は着地したが動作しない。Ruby 4.0.4と4.0.5に対して確認された2つの独立した欠陥がプールを使用不可能にする。完全な調査はdocs/notes/20260520-ractor-pool-cruby-uaf.md

  1. CRubyのheap-use-after-free(上流側、断続的).並列Ractorプールのもとで、あるRactor上のGCスイープがT_DATAオブジェクトを解放し、その同じメモリをrb_vm_ci_lookup(ランタイムのcall-infoパス、すべてのsuper呼び出しから到達)が別のRactor上で並列に読む。AddressSanitizerがそれを特定;Ruby Bug #22075として上流に報告済み。runner_pool_spec.rbの≈70%のランがクラッシュし、クラッシュサイトがランごとに変動する。
  2. Ractor::IsolationError(rigor側、決定論的).ワーカーRactorが非共有可能なプロセスグローバル定数(RBS::EnvironmentLoader::DEFAULT_CORE_ROOT、rigor自身のBuiltins::StaticReturnRefinements::OWNERS_BY_METHODBuiltins::HktBuiltins::METHOD_RETURN_OVERRIDES)を読む。アクセスが発生し、アナライザーがそれをファイルごとにキャッチし、プールが100%のinternal analyzer error診断をemitする — 実際の解析はゼロ。Mastodonベンチマーク(1303ファイル)でシーケンシャルパスの488件の実診断に対して1296件のisolation-error診断が出た。

フォークベースの並列性がworkers > 0のアクティブなバックエンドになる。Ractorプール実装は保持される — それは#22075が修正されshareabilityギャップが閉じられた後の正しい長期方向だ — しかしデフォルトパス上にもはや存在しない;明示的なオプトイン(RIGOR_POOL_BACKEND=ractor)経由でのみ到達可能なので引き続きテスト可能。

  • Runner#analyze_files_in_fork_poolが親で一つのWorkerSessionを構築し、COWでそれを継承するN個の子をforkし、各子がファイルスライスを解析してMarshal化された診断 + レポータードレインを返し、親がマージする。別プロセスは別のGCヒープとvm->ci_tableを意味し(欠陥1から免疫)、COW継承された定数(shareability制約なし — 欠陥2から免疫)。
  • POSIXのみ。forkが利用できない場所(Windows)ではworkers > 0pool-degraded診断付きでシーケンシャルに縮退する。
  • これは下記のWD1を置き換える:フォークはもはや「実現可能なフォールバック」にすぎない訳ではなく、それが出荷バックエンド。WD1の推論はRactor設計に対して有効だった;CRubyの並列Ractorメモリ安全性が4.0.xで本番レディでないとは予期していなかった。

フェーズ1〜3(shareabilityスキャフォールディング)は影響を受けず価値を保つ — WorkerSession基板(フェーズ4a)はバックエンド中立であり、フォークプールがそれを直接再利用する。フェーズ4b/4c(Ractorプール自体)はRuby Bug #22075に加えて定数shareabilityギャップでブロック;両方が解決されたときに再訪する。

RactorはRactor境界を越えるすべてのオブジェクトがRactor.shareable?である必要がある — 凍結 + すべてのフィールドが再帰的に共有可能。このADRがコミットする分割は:

  • 凍結されたサーフェス — 呼び出し元がRactor-sendするものはすべてこのサーフェスにある必要がある。Configuration、Environment(フェーズ2b後)、Scope(フェーズ2b後)、すべての値オブジェクトキャリアがここに乗る。
  • Ractorごとの可変サーフェス — キャッシュ、メモ化テーブル、プラグインの実行ごとの状態。各Ractorが自身のものを所有;データは状態としてRactor境界を越えない。派生した共有可能な値(例: 一度ロードされて共有される凍結されたRBS::Definition)のみが越える。
  • クロスRactor共有可変サーフェス — 正確に1つのクラス: Cache::Store。Monitor + memoレイヤーが共有アクセスを安全にする。StoreのベースディスクはRactor寿命を越えて(およびプロセス寿命を越えて)耐久性があるため、上のRactorごとのキャッシュレイヤーは単一の共有基板からウォームアップする。

WD1 — なぜフォークではなくRactor?

Section titled “WD1 — なぜフォークではなくRactor?”

両方ともGVLをバイパスする。トレードオフ:

側面Ractorフォーク
セットアップコストRactorあたり(約10ms)フォークあたり(macOSで約50〜100ms、Linuxではより低い)
メモリ共有された凍結サーフェス、Ractorごとの可変コピーオンライト(Linux) / フルコピー(macOS)
協調Ractor.yield / Ractor.send / Ractor.takeパイプ + シリアライゼーション
プラグイン形状変更一回限りのリファクタ(フェーズ3)プラグインは無傷で生存(別プロセス)
デーモン / ウォッチモードの再利用直接(Ractorプールが永続化)各リクエストが新たにフォーク
決定性強い(共有可能契約がそれを強制)強い(シリアル化されたIPC)
4.xでのMRI成熟度安定、共有可能性に注意点あり安定、macOSオーバーヘッドあり

Rigorの特定の形 — すべてのファイル間で共有された単一のEnvironment、そのEnvironmentを与えられればステートレスなファイルごとのディスパッチ — に対して、Ractorはより近いフィットです: 共有しない境界が、まさにRigorのデータが分割されたい場所です。フォークは実行ごとにプロセスごとのEnvironment再構築を強制し、インプロセスメモが提供するキャッシュウォーム済みの恩恵を排除する。

フェーズ3のプラグインリファクタが予想より侵襲的だと判明した場合、フォークパスは実行可能なフォールバックになります — フォークはプラグインが共有可能であることを要求しません。私たちはフォークに反対してコミットしているのではありません。Ractorに主要な方向性としてコミットしているのです。

2026-05-20再検討 — 上記のAmendmentにより置き換え。 フェーズ4でRuby 4.0.x上のRactorプールが使用不可能と測定された(Ruby Bug #22075 + isolation-errorギャップ)ため、フォークが今はアクティブなバックエンドであり、 フォールバックではない。このテーブルの「4.xでのMRI成熟度 — 安定、共有可能性に注意点あり」行は 並列Ractorのメモリ安全性に対して楽観的すぎと判明した。「フォークがプロセスごとのEnvironment再構築を強制する」コストも 回避された:フォークプールが親のWorkerSessionを一度構築し、子がCOWで継承する。

WD2 — なぜRbsLoaderを共有可能にするのではなく分割するのか?

Section titled “WD2 — なぜRbsLoaderを共有可能にするのではなく分割するのか?”

RbsLoaderは3つの可変Hashes(@class_known_cache@instance_definition_cache@singleton_definition_cache)プラス内部のRBS::Environment(上流、こちらも可変)を運ぶ。ローダー全体を共有可能にするには次のいずれかが必要:

  • 可変HashesをRactor.make_shareable不変スナップショットに置き換える — キャッシュを打ち破る、呼び出しごとのミスパスがすべてフルenvウォークになる。
  • HashesをRactor::TVarまたは類似に置き換える — MRI 4.xには存在しない。
  • すべてのアクセスにわたる粗いMonitor — 原則として動作するがRactorの共有しないモデルは明示的にこのパターンを拒否する;Monitor-ガードされた共有キャッシュはRactor契約に違反する。

ローダーを分割してEnvironmentが凍結されたリフレクションファサードのみを運ぶようにすると、キャッシュが保たれ(Ractorごとの可変、クロスRactorCache::Storeから取り込まれる)、AND Ractor契約を満たします。コストはリファクタ;代替手段はフェーズ4ワーカープールなし。

WD3 — なぜ共有ではなくRactorごとのキャッシュ?

Section titled “WD3 — なぜ共有ではなくRactorごとのキャッシュ?”

Cache::Storeはプロセス共有のmonitor-safeインメモリレイヤーを提供する。なぜすべてのRactorを直接そこに向けないのか?

Cache::StoreはクロスRactor共有ポイントです — しかしStoreと呼び出しサイトの間にはローダーごとのメモ化レイヤー(@class_known_cacheなど)が座る。メモ化レイヤーはCache::StoreルックアップANDデシリアライズステップを暖かい呼び出しでスキップする。それをクロスRactor共有にするには次のいずれかが必要:

  • すべてのRactorからのメモアクセスごとにMonitorロック — Ractor契約違反。
  • HashをRactorごとの共有不変スナップショットへのビューに置き換える — ミスパスでウォームアップの恩恵を打ち破る。

Ractorごとのキャッシュレイヤーは最初のミスでRactorあたり1つのコールドスタートを払う。Cache::StoreはクロスRactorのウォームアップ(シリアル化されたディスクエントリーAND繰り返しのプロセス内Runner呼び出しのためのインプロセスメモ)をカバーする。組み合わされた動作:

  • 最初のRunner.run、最初のRactor、クラスXに対する最初のclass_known?呼び出し: Cache::Store memoミス → ディスクヒット(または構築) → memo書き込み。ローダー上のRactorごとのmemoがそのRactor内の後続呼び出しの結果をキャッシュする。
  • 2番目のRactor、クラスXに対する最初のclass_known?呼び出し: Cache::Store memo HIT(クロスRactorウォーム) → ディスクなし、Marshal.loadなし。Ractorごとのmemoがワーカーの残りのファイルのためにキャッシュする。

これは2つのレイヤーを正しく構成する。Ractorごとのmemoは小さい(触れたクラスあたり1つのHashエントリー);Cache::Store memoはプール全体で償却される。

WD4 — なぜデフォルトではなくオプトイン?

Section titled “WD4 — なぜデフォルトではなくオプトイン?”

フェーズ4は、例プラグインエコシステムがRactor分離下で検証されるまで、Ractorワーカーをオプトイン(env var / configフラグ)として出荷する。具体的には:

  • フェーズ3のプラグイン契約変更は侵襲的。フェーズ3が着地する前に作成されたプラグインは、共有された可変状態に偶発的に依存している可能性がある(MAY)。ワーカーをデフォルトで有効化すると、それらのバグがユーザーコードで表面化する。
  • ワーカー起動コストは典型的なプロジェクト形状に対してデフォルト化前に測定する必要がある。50ファイルプロジェクトはワーカープールオーバーヘッドのために遅くなるべきではない。
  • 決定性動作(特にプラグイン貢献タイミング周辺)にはまだ存在しないspecカバレッジが必要。

オプトインは早期採用者がトレードオフに注意を払うことを意味する;デフォルトオンは全員が払うことを意味する。検証データがそれを正当化したときにデフォルトオンにする。

WD6 — なぜEnvironment::ReflectionRactor.shareable?ではないのか?

Section titled “WD6 — なぜEnvironment::ReflectionはRactor.shareable?ではないのか?”

フェーズ2b実装中に発見: キャッシュされたinstance_definitions / singleton_definitionsテーブルは、推移的にRBS::Locationを参照する上流のRBS::Definitionオブジェクトを保持する。RBS::LocationRactor.make_shareableが拒否するC拡張状態(lib/rigor/cache/rbs_environment_marshal_patch.rbRBS::Environment Marshalパッチを強制したのと同じ制約)。

解決: Reflectionは凍結 + 読み取り専用 — 不変性の勝利 — だがRactor.shareable?ではない。Ractorワーカープール(フェーズ4)は、各ワーカーが共有されたCache::Storeから自身のReflectionを構築することで制約をサイドステップする。クロスRactor共有ポイントはStoreのディスク + インプロセスメモレイヤー(すでにMonitor-safe);各RactorのReflectionは、同じベースデータのRactorごとの不変な読み取り側ビュー。

将来のRBSリリースがRBS::LocationをRactor共有可能にすれば、ReflectionのinitializeRactor.make_shareable(self)を1行追加すれば、キャリア全体がクロスRactor共有可能になります。それまでは、ワーカーごとのReflectionパターンが契約です。

監査spec(spec/rigor/ractor_readiness_spec.rb)は両方のプロパティを明示的に文書化する: be_frozenアサーションとRBSが制約を解除した日に失敗するnot_to be(Ractor.shareable?)アサーション、1行のアップグレードを促す。

WD5 — スレッドベースの並行性を完全に非推奨にすべきか?

Section titled “WD5 — スレッドベースの並行性を完全に非推奨にすべきか?”

いいえ。Cache::StoreはMonitor + インプロセスメモを保持する。理由:

  • parallel_testsの恩恵(specスイートの6×高速化)はThread様のプロセス分離を使うが、ディスク上でCache::Storeディレクトリを共有する;インプロセスメモとMonitorの両方がプロセスレベルで引き続き有用。
  • 将来のI/Oバウンド作業(例: ネットワークからのプラグインIoBoundaryフェッチ)はThreadを生産的に使える — それらはCレベルI/O中にGVLをリリースする。
  • Monitorコストは競合ゼロパス(シングルスレッドのシーケンシャル解析)でゼロ。

スレッドベースの並列性は非推奨ではない;それはマルチコアCPU利用へのパスではないだけ。

フェーズは上記の順序で着地。各フェーズは独自のコミットクラスタ + specカバレッジを持つ。監査spec(spec/rigor/ractor_readiness_spec.rb)がゲート: ターゲットクラスがskipからパスするRactor.shareable?アサーション(フェーズ1〜2)に切り替わったとき、またはそのエンドツーエンド動作テストがパスしたとき(フェーズ3〜4)にフェーズが進む。

次のフェーズはRbsLoaderを次に分割する:

# 凍結、共有可能
class Environment::Reflection
# 読み取り専用RBSクエリサーフェス
def class_known?(name) end
def instance_definition(name) end
def singleton_definition(name) end
def class_ordering(lhs, rhs) end
# ...
end
# Ractorごと、可変
class Environment::CacheLayer
def initialize(reflection:, cache_store:)
@reflection = reflection
@cache_store = cache_store
@class_known_cache = {}
@instance_definition_cache = {}
@singleton_definition_cache = {}
end
def class_known?(name)
@class_known_cache[name.to_s] ||= reflection.class_known?(name)
end
# ...
end

Environment#rbs_loader(今日)はキャッシュレイヤーになる;新しいEnvironment#reflectionが共有可能なファサードを運ぶ。既存の公開リードAPI(class_known?instance_definitionなど)は変更されないまま — ディスパッチはキャッシュレイヤーを通り、リフレクションファサードを通じて遅延ルーティングされる。

Cache::Storeバックのプロデューサー(RbsConstantTableRbsKnownClassNamesRbsInstanceDefinitionsRbsSingletonDefinitionsRbsClassAncestorTableRbsClassTypeParamNames)は既存の単一blobレイアウトを保つ。リフレクションファサードはそれらの後ろに座る;それらの上のキャッシュレイヤーはRactorごとのウォームアップ。

詳細なスケッチはdocs/design/20260514-ractor-migration.md § フェーズ3 / フェーズ4。ここで繰り返しはスキップ;スケッチはフェーズ2bリファクタが配置され、実際のプラグイン / runner形状制約が見えたら、独自のADR-15修正で批准される。

ADR-4(型推論エンジン)との境界

Section titled “ADR-4(型推論エンジン)との境界”

ADR-4はScope#type_ofScope値オブジェクトを記述する。Scopeはすでに凍結されたEnvironment参照を運ぶ。フェーズ2bリファクタはEnvironmentが何であるかを変える — リフレクションをキャッシュから分割 — が、Scope契約やScope#type_ofの動作は変えない。ADR-4の「実装期待」に従い、ScopeのパブリックAPIはこのリファクタ全体で安定したまま;scope.environment.{class_known?, instance_definition, ...}を読むプラグインは同じ戻り値を見る。

ADR-4はフェーズ2b後にEnvironmentReflection + CacheLayerに分割されることを文書化する非規範的な注記を取得する;Scopeとディスパッチャーの契約は変更されない。

ADR-6(キャッシュ永続化バックエンド)との境界

Section titled “ADR-6(キャッシュ永続化バックエンド)との境界”

ADR-6はCache::Storeflock-ガードされたアトミック書き込みを持つプロセスローカルファイルシステムキャッシュとして記述する。v0.1.4のMonitor + インプロセスメモ追加(コミット31e95c85c30b37)は、スレッドセーフなインプロセスレイヤーリングでそのバックエンドを拡張する。ADR-15はCache::Storeをキャッシュされた値のためのクロスRactor共有ポイントとして指定する。

契約追加:

  • Cache::StoreRactor.shareable?でなければならない(MUST)。現在の実装はそうではない(Monitor + Hash + counter ivarsは共有可能ではない)。フェーズ4の設計が次のいずれかを決定する: (a) Storeを直接共有可能にする、または (b)薄いRactor共有可能なプロキシでラップする。

ADR-6は、将来のCache::Store作業が誤って共有可能性から設計を遠ざけることがないようにこの制約を記録するオープンクエスチョンエントリーを取得する。

ADR-2はプラグイン契約を定義する。フェーズ3変更:

  • プラグインインスタンスはRactorごとになる。プラグインレジストリ(プラグインクラス + マニフェストのシングルトンテーブル)は凍結されたファクトリーリファクタ経由でクロスRactorのまま。
  • プラグインの実行ごとの状態は、クロスRactor協調が必要なときにPlugin::FactStore(すでにMonitor-safe)を介してルーティングすべき(SHOULD)。インスタンスごとのivar状態はRactorごとのまま。

ADR-2は(フェーズ3修正で)、flow_contribution_forまたはdiagnostics_for_fileで可変状態を保持するプラグイン作者がRactorごとの実体化下で安全であるか、Ractor非互換性を文書化しなければならない(MUST)ことを明確にする規範的な注記を取得する。

OQ1 — Cache::Storeは書き込みスループットのためにRactorでシャード化されるべきか?

Section titled “OQ1 — Cache::Storeは書き込みスループットのためにRactorでシャード化されるべきか?”

現在のStoreはすべての書き込みを1つのMonitorを通じて同期する。重い並行書き込み(多くのRactorがすべてコールドパスにヒット)下で、Monitorは競合ポイントになる。Storeをシャード化する(プロデューサーごとまたはキープレフィックスごとのサブストア、各々が自身のMonitorを持つ)と、それが緩和される。

先送り: 最初に測定。期待されるワークロードは読み取り重視(ほとんどのキャッシュヒット)でMonitor競合は無視できるはず。

OQ2 — プラグインのRactorごとのインスタンスは集約状態をどう協調するべきか?

Section titled “OQ2 — プラグインのRactorごとのインスタンスは集約状態をどう協調するべきか?”

rigor-sorbetのabsurd-reachable / reveal-type / assert-type-mismatch追跡はファイルにわたって蓄積する。Ractorごとのプラグイン下では、各Ractorのプラグインインスタンスはそのスライスのみを見る。現在の形はASTノードでキーされたcompare_by_identity Hashesに依存する;ASTノードもRactorごと。

フェーズ3にこれが着地するときの3つの選択肢:

  1. Plugin::FactStore publish/consumeに移行。プラグインは呼び出しごとの観察を公開する;メインRactorはすべてのワーカーが終了した後に集約する。
  2. プラグインごとの結果マージ。各ワーカーがプラグイン状態を診断と一緒に返す;runnerがプラグインごとにマージする。
  3. 並列性からのプラグインオプトアウト。プラグインがmanifest(serial: true)を宣言し、runnerがそれらへの呼び出しをシリアライズする。

決定はフェーズ3に先送り。

OQ3 — RactorプールサイズはCPU数派生または設定可能であるべきか?

Section titled “OQ3 — RactorプールサイズはCPU数派生または設定可能であるべきか?”

両方。[CPU_count - 1, 4].min(親 + OS用に1コアを残す)にデフォルト;RIGOR_RACTOR_WORKERS.rigor.ymlparallel: { workers: N }を尊重する。specスイートですでに動作したparallel_testsのノブ形状と同一。

  • 現状(シングルスレッドアナライザー): 中規模および大規模プロジェクトに対してwall-clock影響が大きく、監査データ(157ファイル、1.8秒ウォーム)がヘッドルームが利用可能であることを示すため拒否 — 私たちはそれを床に残しているだけ。
  • 純粋なフォークベースのワーカー: 完全に拒否されていないが二次として考慮。フォークは起動コストが高く、デーモンパスがなく、フォークごとのEnvironment再構築を強制する。Ractorパスはより多くの下流ケース(LSP、ウォッチモード、将来のrigor server)を解決する。
  • gem(例: concurrent-ruby)経由の外部ワーカープール: 拒否。GVL問題を解決せずに依存関係を追加する;MRI 4.x下でconcurrent-rubyスレッドはCPU作業に対して引き続きGVLバウンド。
  • Ruby M:Nスケジューラの成熟を待つ: ブロッキングとして拒否。M:Nスケジューラは存在するがMRI下のCPU並列性ストーリーはまだ進化中。Ractorは今日コミットされ安定。
  1. ✅ フェーズ1 — 値オブジェクトの共有可能性。
  2. ✅ フェーズ2a — Configurationディープフリーズ。
  3. ✅ フェーズ2b — Environment::Reflection抽出(凍結、まだRactor共有可能ではない;RBS::Location制約についてはWD6を参照)。
  4. ✅ フェーズ3a — Plugin::Blueprint + Registry#blueprints + Registry.materializeファクトリー。ライブプラグインインスタンスは意図的に共有可能ではない;ブループリントセットがクロスRactorハンドル。
  5. ⏭ フェーズ3b — クロスRactorプラグイン集約状態契約(§ OQ2を参照)。フェーズ4がワーカーごとのプラグイン状態の実際の形を測定するまで先送り。
  6. ✅ フェーズ4a — Analysis::WorkerSession値キャリア;ループ内にまだRactorなしのワーカーごとの基板。§ フェーズ4設計 + 設計ドキュメント § フェーズ4aを参照。
  7. ✅ フェーズ4b — WorkerSession周りのRunner Ractorプール(プログラム的なworkers:キーワード;シーケンシャルが引き続きデフォルト;CLI / .rigor.ymlオプトインは4cに先送り)。
  8. ✅ フェーズ4c — RIGOR_RACTOR_WORKERSオプトインフラグ + .rigor.ymlparallel.workers:エントリー + Configuration#parallel_workersアクセサ + CLI --workers=Nフラグ(優先順位: CLI > env > config > 0)。デフォルトは引き続きシーケンシャル。プールspecはデフォルトスイートから除外(§「既知の制限」を参照)。
  9. ✅ フェーズ4b.x — キャッシュpre-warm経由のワーカー側env構築安定性。Runner#analyze_files_in_poolは今、ワーカーをスポーンする前にメインRactorですべてのキャッシュ済みRBSプロデューサーを駆動する(RbsLoader#prewarm)ため、ワーカーはディスク上のMarshal blobからすべてのリフレクションクエリを提供し、RBS::EnvironmentLoader.newに触れることはない — 非共有のRubyGems / RBSモジュール定数のチェーンはメインRactorのみにとどまる。Rigor自身のディスパッチ定数(MethodDispatcher::ConstantFolding::CATALOG_BY_CLASSなど)はRactor.make_shareableになった;Builtins::MethodCatalogはYAMLを積極的にロードするため、make_shareableの凍結が遅延@catalog ||= load_catalog書き込みをトリップしない。cache_storeなしのプールモードはpool-degraded :warning診断付きでシーケンシャルに縮退する — --no-cacheパスはレガシーセマンティクスを保つ。
  10. ⏭ フェーズ4c+ — § OQ1に従いワーカーごとのCache::Store共有ファサード;シーケンシャル対プールのwall-clockをベンチマークし、フェーズ4b.xがワーカーenv構築を安定化させたらデフォルトを再検討。

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.