ShareLockのオーナーシップをIsolatedExecutionStateに統一し、Fiberアイソレーション下の競合状態を修正
ActiveSupport::Concurrency::ShareLockがロックの所有者をThread.currentで識別していたため、:fiberアイソレーション下では同一スレッド上の複数のFiberが同じ所有者として扱われていました。このPRはオーナーシップの識別をActiveSupport::IsolatedExecutionState.contextに切り替えることで、Fiberスケジューラ環境(Falconなど)での定数破壊バグを修正します。
背景
config.active_support.isolation_level = :fiberを設定した環境では、同一スレッド上の複数のリクエストFiberがShareLockから単一の所有者として見えていました。この問題により、リローダーのインターロックが正常に機能しなくなっていました。
具体的には、あるFiberがshareロックを保持している状態でも、別のFiberによる排他的な:unloadロックの取得が許可されてしまいます。その結果、リクエスト処理の途中でオートロードされた定数がクリアされ、ビューで予期しないNoMethodErrorが発生します。この症状はFalconのようなFiberスケジューラを使用するサーバーで、起動後の初回ファイル変更時に顕在化します。
一方、CurrentAttributesやその他のフレームワーク機能は既にActiveSupport::IsolatedExecutionState.contextを「現在の実行コンテキスト」の識別に使用しています。ShareLockだけがThread.currentを使い続けていたことが、この不整合の原因でした。
技術的な変更
ShareLockのオーナーシップ識別をThread.currentからIsolatedExecutionState.contextに変更し、関連する変数名・フィールド名をthread中心の命名からowner中心の命名に統一しました。
activesupport/lib/active_support/concurrency/share_lock.rbでは、@exclusive_threadを@exclusive_ownerに改名し、内部でスレッドを管理していた@sleeping・@sharing・@waitingの各Hashのキーも同様にオーナーオブジェクトとして扱うように変更されています。raw_stateメソッドの返り値も:threadキーが:ownerキーに変更されました。
変更前:
threads = @sleeping.keys | @sharing.keys | @waiting.keys
threads |= [@exclusive_thread] if @exclusive_thread
data[thread] = {
thread: thread,
sharing: @sharing[thread],
exclusive: @exclusive_thread == thread,
...
}
変更後:
owners = @sleeping.keys | @sharing.keys | @waiting.keys
owners |= [@exclusive_owner] if @exclusive_owner
data[owner] = {
owner: owner,
sharing: @sharing[owner],
exclusive: @exclusive_owner == owner,
...
}
ActionDispatch::DebugLocksはraw_stateの唯一の利用者であるため、同様に変数名をthreadからownerに変更しています。加えて、Fiberにはstatusメソッドが存在しないためFiber#statusの呼び出しでクラッシュしていた問題も修正されました。表示メッセージはスレッドとFiber両方に対応する汎用表現に変更されています。
変更前:
msg = +"Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n"
変更後:
msg = +"#{owner.class} #{info[:index]} [0x#{owner.__id__.to_s(16)} #{owner_status(owner)}] #{lock_state}\n"
テストとしてShareLockFiberTestが追加されました。isolation_levelを:fiberに設定したうえで、start_exclusive(no_wait: true)と手動のFiber.resumeを組み合わせることで、Fiberスケジューラなしに自己完結したかたちで所有権セマンティクスを検証しています。同一スレッド上の2つのFiberが独立した所有者として認識されること、および既存の:threadアイソレーションでは複数のFiberが単一オーナーに集約されることの両方を確認しています。
設計判断
既存のIsolatedExecutionState.contextを採用することで、フレームワーク全体での「現在の実行コンテキスト」定義との一貫性を確保しました。
PRの説明にあるように、CurrentAttributesなど他のフレームワーク機能が既にこのAPIを使用しているため、ShareLockも同じスコープを使うことは自然な選択です。:threadアイソレーション(デフォルト)ではIsolatedExecutionState.contextがThread.currentと同じオブジェクトを返すため、既存の動作は変わりません。変更の影響はFiberアイソレーションを明示的に設定した場合のみ現れます。
変数名のthread→ownerへの改名はリネームに留まらず、保持するオブジェクトの型がFiberである可能性を明示的に認めるという意味を持ちます。PRの説明でも「フィールドがFiberを保持している場合にスレッドと主張すべきではない」と述べられており、APIの正確さを重視した判断といえます。
まとめ
この修正は、ShareLockのオーナーシップ識別を1箇所変更することで、Fiberスケジューラ環境での深刻な競合状態を解消しています。IsolatedExecutionState.contextへの統一により、Railsフレームワーク全体で「実行コンテキスト」の概念が一元化され、今後の新たなアイソレーションレベルへの対応も容易になります。