おもしろwebサービス開発日記

Ruby や Rails を中心に、web技術について書いています

ファイバーストレージの紹介と注意点

以前のエントリで、スレッドローカル変数とファイバーローカル変数について解説しました。このエントリはその続きになります。

ファイバーストレージとは

スレッドローカル変数やファイバーローカル変数を使うと複数(スレッド|ファイバー)環境で固有の値を持つことができて便利です。利用例としてはActiveSupport::CurrentAttributesなどがあります。

しかし、(スレッド|ファイバー)ごとに固有の値を持つことで不便を感じるケースがあります。例えばRailsなどでリクエストを受け付けている最中に別の(スレッド|ファイバー)を作り、その中で外部APIを叩くとします。このときに外部APIを叩く(スレッド|ファイバー)からリクエストを処理する(スレッド|ファイバー)で設定した(スレッド|ファイバー)ローカル変数を参照することはできません。これは不便ですね。

この問題を解決したのがRuby3.2から導入されたファイバーストレージです。ファイバーストレージとしてアサインされた変数は、子の(スレッド|ファイバー)を作ったときに親のコピーを子のファイバーストレージとして設定する、という動きをします。導入したPRはこれ *1。「ファイバーストレージ」という名前からスレッドとは関係なさそうな雰囲気を感じますが、スレッドを新規作成したときでも同様です。

具体的な使い方についてはRuby 3.2 - Fiber - tmtms のメモが詳しいです。

Railsの大半のユースケースにおいては、(スレッド|ファイバー)ローカル変数よりもファイバーストレージの方が使い勝手が良いように思います。しかし、扱いには注意が必要です。

RequestStore1.6.0でのファイバーストレージの利用例

RequestStoreというgemがあります。これはActiveSupport::CurrentAttributesと同じようにリクエストごとにリセットされるグローバルな変数を扱うためのものです*2。RequestStore内部ではファイバーローカル変数が使われていましたが、RequestStore 1.6.0で、ファイバーストレージが使える環境(Ruby >=3.2.0)であればそれを使うという変更が入りました

しかしそれによって、RequestStore1.6.0を使うとsidekiqでときどき設定したはずの値が消えるぞ、というIssueがたちました。なぜでしょうか。

v1.6.0のRequestStoreはこのファイルを見るとわかるのですがFiber[:request_store]{}をアサインしてそれをRequestStore用の変数として使う、という実装になっています。これは一見問題ないように見えますが、使い方によってはスレッドセーフではなくなってしまいます。

前提知識

  • ファイバーストレージの実態はハッシュ
  • ファイバーストレージの実装は、子の(スレッド|ファイバー)を作るときに「親のファイバーストレージをdupして子の(スレッド|ファイバー)のファイバーストレージにする」というもの
  • dupはshallow copyなので、ファイバーストレージのハッシュオブジェクトは(スレッド|ファイバー)ごとに別物になるが、ハッシュが持つオブジェクト自体は同じものを指してしまう

RequestStore1.6.0でスレッドセーフが壊れる使い方の例

まず、sidekiqなどでRailsアプリケーションを起動します。Rails起動時にRequestStore.storeとすると、Fiber[:request_store] = {}のようにファイバーストレージへの値のアサインが実行されます。

sidekiqはRails起動後にワーカスレッドを複数作ってジョブを処理していきます。するとすべてのスレッドでFiber[:request_store]は同じハッシュオブジェクトを指すので、一つのスレッドで行った変更が他のスレッドにも反映されてしまいます。

肝は、親(スレッド|ファイバー)にあたる箇所でFiber[:request_store] = {}を実行している、という点です。これを避ければ子(スレッド|ファイバー)でFiber[:request_store]が指すオブジェクトは別々になりスレッドセーフが保たれます。

おそらく近日中にファイバーストレージを使わない形に変更したRequestStore v1.7.0(もしくはv1.6.1)がリリースされるのではないかと思いますが、もしお手元のプロジェクトでRequestStore v1.6.0を使っていたら使い方を確認の上、場合によってはv1.5.1に切り戻しておいたほうがいいかもしれません。

所感

最新のRailsのActiveSupport::CurrentAttributesはスレッドローカル変数とファイバーローカル変数のどちらかを設定によって使い分けるようになっていますが、これがファイバーストレージに変わる未来もあるのかも?と思っています。しかし上記で示した課題があるので、安易に移行するのも難しい。なにかしらうまい解決方法はないかなあ…となっている今日このごろです。

*1:ここから別PRで振る舞いが修正されているので、コードに興味ある人は最新版を参照した方が良いです

*2:もともとRequestStoreが先にあって、Rails公式機能として後に導入されたのがActiveSupport::CurrentAttributesなはず(要出典)