こんにちは!
イタンジ株式会社でバックエンドエンジニアをしている藤崎 (https://x.com/aki19035vc) です。
イタンジの各種サービスの要である物件基盤システムを開発をしています。
Rails 7.2が今にも出そうな気配がしていたので、置いていかれないように私がメインで見ているRailsアプリケーションは7.1に上げました。
その際、いくつか気をつけるべきポイントとハマりポイントがあったのでその共有も兼ねて記事を書こうと思います。
前提
今回アップデートしたRailsアプリケーションの特性は下記の通りです。
- Ruby 3.3.3
- Rails 7.0.6
- APIモードで動作しており、レスポンスはJSONしか返さない
- テストのラインカバレッジは(ほぼ)100%
- 型の記載率は(ほぼ)100%
記載の通りテストのラインカバレッジを100%に維持しています。
また、型に関しても「appディレクトリ以下」は100%書かれています。
ハッシュの中身などは untyped
していますが、メソッドの引数や戻り値そのものを untyped
にしている箇所はありません。
メタプロしている場合はgeneratorを作ったり自前で頑張って書いたりしています。
そのため、既存のテストや型検査が通ればアプリケーションに重大な破綻が起きていない事に自信を持つ事ができます。(フラグ)
Rails 7.1にアップデート!
特に難しいことはありません。Railsアップグレードガイドにしたがって作業をします。
https://railsguides.jp/upgrading_ruby_on_rails.html
Railsをアップデート後、アップデートタスクを実行します。
$ bundle udpate rails $ bin/rails app:update
生成される設定ファイルはコンフリクトしますが、1度全て受け入れてコミットしてしまいます。その後、既存の設定との差分を見比べながら手作業で直していきました。
また、このタイミングでnew_framework_defaults_7_1.rb
に記載されている設定のコメントアウトを、1つずつ確認しながら外していきます。
config.active_support.cache_format_version
今回影響がありそうなデフォルト設定の変更は、キャッシュフォーマットバージョン (config.active_support.cache_format_version
) の部分だけでした。
Railsガイドには下記のように書かれているのですが、ここの「前方互換性」という表現には少し注釈が必要です。
https://railsguides.jp/v7.1/configuring.html#config-active-support-cache-format-version
どの形式も前方互換性と後方互換性があります。 つまり、ある形式で書かれたキャッシュエントリは別の形式で読み取り可能です。 この振る舞いによって、キャッシュ全体を無効化せずに異なる形式間の移行を楽に行えるようになります。
Rails 7.0で読み取ることができるのは、Rails 7.1で cache_format_version = 7.0
の状態で生成されたキャッシュのみだからです。
Rails 7.1でcache_format_version = 7.1
の状態で生成されるキャッシュは、Rails 7.0で読み取ることはできません。
※ 読み取れる場合もあるようですが、危険なのでやめましょう。
Rails アップグレードガイドの 6.1 -> 7.0 には記述があるので、こちらが参考になります。
new_framework_defaults_7_1.rb
の中のコメントには書かれているのですが、Rails アップグレードガイドの 7.0 -> 7.1 には書かれてないので、ここで取り上げておきました。
ハマりポイント
アップデートで必要な対応が終わったためステージング環境でしばらく様子を見ていたのですが、いくか問題が発生してしまい、変更をリバートすることがありました。
それぞれについて原因・対応・対策を記載していきます。
エラーがSentryに送信されない
弊社では、Railsアプリケーション上で発生したエラーはSentryに送るようになっているのですが、ここが上手く動いていませんでした。
ログを見てみると、
got #<NoMethodError: undefined method `show_exceptions?' for an instance of ActionDispatch::Request>
どうやら、Rails7.1からActionDispatch::Request#show_exceptions?
がなくなったのが原因のようでした。
対応
この問題の対応方法は簡単で、sentry-railsも最新バージョン(※ 記事執筆時点では5.17.3)にアップデートするだけです。
bundle update sentry-rails
対策
この問題をテストで検知できなかったのはSentryの初期化処理をローカル環境ではスキップしていたことが原因でした。
ラインカバレッジが100%なのはappディレクトリ以下のファイルであり、config/initializers以下のファイルは計測対象に含まれていなかったのも要因の1つです。
そのため、どの環境でもSentryの初期化処理が走るようにしつつ、手元の環境ではSentryに送信されないように変更し、テストで今回の問題を再現できるようにしました。
# config/initializers/sentry.rb Sentry.init do |config| config.environment = ENV.fetch('SENTRY_ENV', 'local') config.enabled_environments = %w[production staging] # -- 省略 -- end
ちなみに
sentry-railsの実装が気になったので見てみましたが、内部では下記のようにRailsのバージョンが7.1かどうかで処理を分岐しているようでした。
module Sentry module Rails class CaptureExceptions < Sentry::Rack::CaptureExceptions # -- 省略 -- def show_exceptions?(exception, env) request = ActionDispatch::Request.new(env) if RAILS_7_1 ActionDispatch::ExceptionWrapper.new(nil, exception).show?(request) else request.show_exceptions? end end end end end
Rails.cache.redis
がRedisインスタンスを返さなくなった
本アプリケーションではキャッシュストアとしてRedisを使用しています。
キャッシュを扱う際は基本的にActiveSupport::CacheStore
のインターフェースを使うのですが、一部処理ではRedis特有の機能を使用したく、Rails.cache.redis
で取得できるRedisインスタンスを使用している箇所がありました。
しかし、Rails 7.1からはRails.cache.redis
で返ってくる値がRedis
ではなくConnectionPool
のインスタンスになってしまい、Redis
インスタンス前提の処理が動作しなくなってしまいました。
下記がRailsコンソールで実際に試してみた結果です。
# === Rails 7.0 === $ rails c Loading development environment (Rails 7.0.6) irb(main):001> Rails.cache.redis => #<Redis client v4.6.0 for redis://redis:6379/1> # === Rails 7.1 === $ rails c Loading development environment (Rails 7.1.3.4) irb(main):001> Rails.cache.redis => #<ConnectionPool:0x0000ffff750cbaf0>
Railsアップグレードガイドには
2.5 MemCacheStoreとRedisCacheStoreがデフォルトでコネクションプールを使うようになった
という記載はあるのですが、今回の件に関係しそうな記述は見当たりませんでした。
なお、このConnectionPool
というのは下記の connection_pool gem で提供されているものになります。
https://github.com/mperham/connection_pool
対応
connection_pool gem のREADMEに記載されているように、#then
か#with
メソッドを使うことで、この問題を解消することができます。
Rails.cache.redis.with do |redis| redis.set(key, value) end
今回の修正では#with
メソッドを使用する形にしました。
#then
メソッドがRubyのあらゆるオブジェクトで使用できるメソッドであるため、#with
メソッドを使用することで「少し特殊」という事を読み手に伝えられると考えたためです。
対策
この問題に気がつけなかった原因は、テストではRedisを使っておらず、Rails.cache
で返ってくる値をモックしてしまっていたことです。
そのため、テストでもキャッシュストアとしてRedisを使うように変更しました。
ただし、parallel_tests gem を利用してテストを並列で実行しているため、下記のように使用するRedisのDBを被らないようにする必要はありました。
# config/environments/test.rb Rails.application.configure do # Avoid db number used in development redis_db_num = Integer(ENV['TEST_ENV_NUMBER'].presence || '1', 10) + 10 config.cache_store = :redis_cache_store, { url: "#{ENV.fetch('REDIS_URL')}/#{redis_db_num}" } end
また、下記はRSpecの例ですが、各テストケースごとにキャッシュがクリアされるようにshared_contextを定義して使用する形にしました。
# spec/support/using_cache_store.rb shared_context 'when using cache store' do around do |example| Rails.cache.clear example.run Rails.cache.clear end end
ちなみに
RedisCacheStoreの生成処理は以下の通りで、「コネクションプールを使わない」という設定を明示している時はRedis
インスタンスを生成する形になっていました。
Rails.cache
の型がuntyped
ではなくなったとしても今回の件は型では気がつけないですね、、、
module ActiveSupport module Cache class RedisCacheStore < Store # -- 省略 -- def initialize(error_handler: DEFAULT_ERROR_HANDLER, **redis_options) universal_options = redis_options.extract!(*UNIVERSAL_OPTIONS) if pool_options = self.class.send(:retrieve_pool_options, redis_options) @redis = ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) } else @redis = self.class.build_redis(**redis_options) end @max_key_bytesize = MAX_KEY_BYTESIZE @error_handler = error_handler super(universal_options) end # -- 省略 -- end end end
まとめ
本記事では、Railsアプリケーションを7.1にアップデートする際の注意点と、実際に発生したハマりポイントをご紹介いたしました。
テストや型があってもハマる時はあるので、何か問題が起きた時にすぐリバートできるようにしておくのと、今後同じ事を起こさないように対策を入れるようにしましょう。
7.1へアップデートする際の参考に少しでもなれば嬉しい限りです。
最後に
Rails 8.0 が楽しみですね。デフォルトのジョブキューバックエンドになると思われる Solid Queue に特に注目しています。
https://github.com/rails/solid_queue
弊社ではSidekiqを使用することが多いですが、乗り換え先となりえるのかどうか、、、
実際に触ってみたら、またブログで共有しようと思います。