Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

小さなActiveSupportモジュールで実現する、より良いグローバル変数管理

一般に、グローバル変数は避けるべきものとされています。しかし、適切な場所で使えば、コードを大幅にシンプルにできることもあります。

Railsアプリでは、リクエスト中に一度だけデータを設定し、それをアプリのあらゆるレイヤーで利用したい場面があります。リクエストしたのはどのユーザーか?どんな権限を持っているか?どのデータベース接続を使うべきか?

こうした情報はコード全体から参照できるべきものなので、あちこちに引数として渡すのはノイズでしかありません。しかし、Rubyのグローバル変数やクラス変数を使うと、今度は別の問題が発生します。複数のスレッドが同じ変数を上書きしてしまい、大混乱に陥り、最悪の場合、取り返しのつかない不正なデータになってしまうかもしれません。

必要なのは、「自分のリクエストだけ」に有効なグローバルな仕組みです。

素のRubyでのやり方

通常、この問題はThread.current[]で処理されることが多いでしょう。次のようなコードです。

Thread.current[:current_user] = user

シンプルで、まずまずの解決策ではあります。しかし、大きな欠点が2つあります。

  1. 誰かがうっかりデータを上書きしてしまう恐れがある

    もし2人が同じキーを選んでしまったら、お互いのデータを上書きし合うことになります。ユーザー情報のような重要なデータを保存している場合、これは深刻な問題です。自分のアプリ内であれば、あまり問題にならないかもしれません。しかし、gemを開発している場合や、規模が大きく混沌としたRailsアプリでは、真剣に考慮すべき点です。

  2. 構造化されていない

    Thread.current[]は単なる巨大なデータの入れ物です。ドキュメント化も容易ではなく、何を取り出せるのかを知るには、何を入れたのかをコード全体から探し回るしかありません。

    もちろん、これが問題になるほど大量のグローバルデータを保存しているなら、Thread.current[]の構造の欠如以上に心配すべきことがあるはずです。それでも、頭の片隅に置いておきたいポイントではあります。

では、Thread.current[]より良い解決策はあるのでしょうか?ご想像の通り、Rails自身も大量のリクエストローカルなデータを管理しています。そして宝箱のようなActiveSupportには、別のパターンが用意されているのです。

Rails流のやり方

ActiveSupportのコードを掘り下げていくと、ActiveSupport::PerThreadRegistryというモジュールに出会います。名前からも分かる通り、まさに私たちが探していたものです。

ActiveSupport::PerThreadRegistryを使うと、先ほどの例は次のように書けます。

class RequestRegistry
  extend ActiveSupport::PerThreadRegistry

  attr_accessor :current_user, :current_permissions
end

RequestRegistry.current_user = user
RequestRegistry.current_user # => user

ほんの少し手間は増えますが、ActiveSupport::PerThreadRegistryには次のようなメリットがあります。

  • ドキュメントを書く場所ができる。クラスを見れば、どんなグローバルデータを期待しているのか、何を利用できるのかが一目瞭然です。

  • 見た目の良いAPIが手に入る。実際、まるでクラス変数を設定しているように見えます。スレッドを気にしなくてよいなら、そう書いていたはずです。

  • クラスに設定したデータは名前空間で分離される。RequestRegistry.current_userPhoneNumberApiRegistry.current_userが衝突する心配はありません。完全に独立して扱われます。

Rails自身も内部でActiveSupport::PerThreadRegistryを使用しています。例えば、ActiveRecordのコネクション管理、EXPLAINクエリの保存、ActiveSupport::Notificationsなどです。PerThreadRegistryの活用例をもっと見たいなら、Rails本体のソースコードが最高の教材になります。

スレッドとリクエストが一致しないとき

一部のRailsサーバーでは、1つのスレッドが複数のリクエストを処理します。つまり、PerThreadRegistryに入れたデータは、次のリクエストにも残ってしまうということです。これはおそらく望んでいる動作ではないでしょう。

Railsと同じように、使い終わったらPerThreadRegistryの中身を掃除する方法もあります。保存する内容によっては、そもそも後始末が必要な場合もあるでしょう。

コメント欄では、MattBさんがより簡単な解決策としてrequest_storeを紹介しています。Thread.current[]のように振る舞いながら、リクエストごとに自動的にクリアされるgemです。

request_storeの安全性とPerThreadRegistryのAPIを組み合わせた「PerRequestRegistry」を作る余地は十分に残されています。すでに誰かが実装しているなら、ぜひ教えてください!

試してみよう

グローバルデータは危険になり得ます。グローバル変数が多すぎると、あっという間に保守困難なコードになってしまいます。

しかし、リクエスト全体を通じてデータを一箇所に保持するのが理にかなっているなら、Thread.current[]の一歩先へ進みましょう。ActiveSupport::PerThreadRegistryやrequest_storeを試してみてください。少なくとも、グローバルデータの扱いが少し安全になるはずです。

補足:新しいRailsバージョンでの扱い

なお、ActiveSupport::PerThreadRegistryはRails 5.0で非推奨となり、5.1で削除されました。現在のRailsでは、thread_mattr_accessorActiveSupport::IsolatedExecutionStateが代替手段として提供されています。ただし、リクエストローカルなデータを安全に管理するというこの記事の考え方は、今でも変わらず有効です。


  1. ハッカーはハッキングされたウェブサイトで何をするのか?攻撃後の手口を徹底解説

    2017年は、サイバー犯罪の歴史においても特筆すべき年となりました。CMS(コンテンツ管理システム)やECポータル、金融機関のウェブサイト改ざんに至るまで、サイバー犯罪は年々増加の一途をたどっています。2017年には、約1億4,300万人分の個人情報が流出した「Equifax情報漏洩事件」、同年最悪とされたランサムウェア攻撃「WannaCry」、その影響を広げた「Petya」攻撃、そして数十億アカウント規模の被害が出た「Yahoo!情報漏洩事件」など、衝撃的なサイバー攻撃が相次ぎました。 ウェブサイトはどのようにハッキングされるのか? Googleによると、ハッキングされたウェブサイトの数は3

  2. ビッグデータの落とし穴とは?知っておきたい3つの主要な問題点

    ビッグデータは、機械学習とアルゴリズムを中核とし、高い需要と優れた機能性により、現在まさに絶頂期を迎えています。ビッグデータ分析ソリューションの導入を目指す企業が増え、この分野の専門人材への需要も高まり続けています。予測モデルや統計分析に基づくビジネスインテリジェンスの獲得を競う企業が多いなか、ビッグデータの文化は世界を席巻し、一つの標準として確立されつつあるのです。 データが指数関数的に増加するにつれ、ビッグデータ、IoT、クラウドコンピューティングといった最先端テクノロジーへの需要も急拡大しています。専門家によれば、これらの技術は近い将来、あらゆるビジネスに不可欠な存在になるとされています