Fix `pool_capacity` leak when extra IO threads exit
この PR は、puma.mark_as_io_bound でマークされた I/O バウンドスレッドが max_threads を超えて終了した際に、スレッド数カウンタ @spawned が減算されず pool_capacity が 0 のままになるリークを修正します。
背景
I/O バウンドスレッドが上限を超えて終了したときに @spawned がデクリメントされない ため、Puma のスレッドプールが正しい容量情報を提供できなくなる問題が報告されました。#3958 では、workers 0, threads 5,10, max_io_threads 10 の構成で 20 件の同時リクエストを投げると、puma_thread_pool_capacity が 0 のまま回復せず、リクエストがスタックする現象が示されています。スレッドが終了しても @spawned が増え続け、結果としてプールが新規リクエストを受け付けられなくなる状態です。
この挙動は、puma.mark_as_io_bound によって I/O バウンドとして扱われたスレッドが、max_threads 制限を超えたときに余剰スレッドとして即座に Thread.exit するコードパスにおいて、カウンタの減算処理が欠落していたことが原因です。
問題の根本 は、既存の trim ロジックと同様に余剰スレッド退出時に @spawned を減らす処理が無かった点にあります。これにより、プール容量が正しく回復せず、サーバーがスレッド枯渇状態に陥ります。
技術的な変更
spawn_thread メソッドに @spawned -= 1 を追加し、I/O バウンドスレッドが上限を超えて退出するコードパスでカウンタを減算するよう修正しました。コード変更は次の通りです。
# We're already at max threads, so we exit the extra io thread.
+ @spawned -= 1
@processors.delete(processor)
trigger_before_thread_exit_hooks
Thread.exit
この 1 行の挿入は、余剰スレッドが退出する唯一のパスに対して行われ、他のロジックへの影響はありません。
テストスイートにも test_extra_io_threads_release_spawned_count_on_exit を新規追加し、@spawned が正しく減算され pool_capacity が回復することを検証しています。
def test_extra_io_threads_release_spawned_count_on_exit
mutex = Mutex.new
mutex.lock
queue = Queue.new
pool = new_pool(0, 2, max_io_threads: 2) do |processor, io_bound_thread|
processor.mark_as_io_thread! if io_bound_thread
queue << Thread.current
mutex.synchronize { }
end
@@
4.times { pool << true }
4.times { queue.pop }
assert_equal 4, pool.spawned
@@
Timeout.timeout(1) { sleep 0.01 until pool.spawned == 2 }
assert_equal 2, pool.spawned
assert_equal 2, pool.pool_capacity
ensure
mutex.unlock if mutex.owned?
pool.shutdown(1)
end
テストは、余剰 I/O スレッドが生成された後に全スレッドが一時停止し、解除されたときに spawned が期待通りに減少し、pool_capacity が正しい値に復元されることを確認します。
設計判断
最小侵入的な修正 が選択されました。trim ロジックと同等のカウンタ減算を直接 spawn_thread に挿入することで、既存のフローを乱さずにバグを解消できます。代替案としては trim メソッドを共通化して呼び出す、あるいはカウンタ管理用のラッパークラスを導入するといった抽象化が検討されましたが、パフォーマンスと可読性への余計なコストが懸念されたため見送られました。
このアプローチは 後方互換性を保持 しつつ、変更箇所を 1 行に留めることでリスクを最小化します。既存の設定やコードベースに影響を与えることなく、正確なスレッド数管理が復元されます。
まとめ
余剰 I/O バウンドスレッドが退出した際に @spawned カウンタが正しくデクリメントされるようになり、pool_capacity のリークが解消されました。これにより、Puma のスレッドプールは正確な容量情報を提供し続け、IO バウンドワークロードが急増したシナリオでもサーバーの安定稼働が期待できます。