Rails

minne の Rails を 5 にアップグレードしました

Rails

minne 事業部チーフテクニカルリードの @_shiro16 です。

おかげさまで minne は 1 月 17 日に 5 周年を迎えました。その 1 週間後の 1 月 24 日に Rails4.2 から 5 へのアップグレードを行いました。 そこで今回はアップグレードを行う際に行ったことの一部をご紹介します。

はじめに

今回は基本的なアップグレード手順の説明は省略し、minne ではどのように進めていったかを解説していきます。 アップグレード作業は主にチーフエンジニアの @hsbt と僕の 2 人で進めました。

まず minne がどの程度のコード量なのかをご覧いただこうと思います。

minne の rake stats の結果は以下の通りです。

image

基本方針

  1. DEPRECATION WARNING は出来るだけ消す
  2. 可能なものは積極的に backport

上記のような 2 つの大きな方針でアップグレードを進めました

DEPRECATION WARNING は出来るだけ潰す

DEPRECATION WARNING(以下 WARNING) はいずれ廃止されるものなので、すぐに修正を行う必要はないのですが今対応するか後で対応するかの違いだけなので出来るだけ潰しました。

使用している gem で WARNING が発生していた場合は gem の version up を積極的に行い WARNING が発生しないようにしました。 対応した WARNING の修正の一部を列挙すると下記のようなものになります。

  • alias_method_chain を Module#prepend に変更 PR
  • uniq を distinct に変更 commit
  • controller の env を request.env に変更 commit
  • render :text を render :plain or render :html or render :body に変更 PR
  • original_exception を Exception#cause に変更 PR
  • render status: :ok, nothing: true を head :ok に変更 PR
  • redirect_to :back を redirect_back に変更 PR
  • ActionController::TestRequest の変更に伴い spec での引数を変更 commit commit
  • ActionDispatch::Integration::Session の変更に伴い spec での引数を変更 commit

ActionController::TestRequest の変更に伴い spec での引数を変更ActionDispatch::Integration::Session の変更に伴い spec での引数を変更 に関しては synvert と自作の synvert-snippets を使用して一括で置き換えを行いました。 この snippets では完璧に置き換えを行なってくれるものではないですが、minne のコードであれば 95% 以上正確に置き換えを行なってくれるので漏れた分に関しては手動で書き換えを行いました。

WARNING は出来るだけ潰すという方針でしたが、1 箇所だけ WARNING が発生することを許容した箇所があります。それが callback chain の変更です。

Rails4 までは callback method 内で false を返せば callback chain を止めることが出来たのですが、Rails5 では明示的に throw(:abort) を呼ぶことが推奨されるようになりました。

ActiveSupport.halt_callback_chains_on_return_false = true

上記のように記述しておけば WARNING が出ますが Rails4 と同じ挙動をしてくれるので、この設定を行い WARNING を許容することにしました。

理由としては minne では決済処理などのお金が絡む処理を行うのでいきなり全ての callback chain の変更を行うのに不安がありました。 そこで Rails5 への移行が完了した後に徐々に修正を行っていこうという方針にしました。

可能なものは積極的に backport

backport 可能なものは Rails4.2 で本番稼働中である minne の master branch(以下 master) に積極的に backport を行い Rails5 対応 branch との差分を出来るだけ少なくするようにしました。

理由としては差分が少なくなることでレビュー等の際に見る範囲が絞られ質の高いレビューが出来ること、master に取り込みリリースしておくことで問題なく動作しているという安心感を得られること、 そしてこれが一番大きな理由ですが、コンフリクトの発生や追加の WARNING の発生が抑えられることです。

Rails5 の対応を行っている間にも多くの変更が master に merge されるので、頻繁にコンフリクトが発生したり Rails5 対応 branch に master を merge した際に新たに WARNING が多々発生してしまうという問題があったのでこのような方針にしました。

backport を行なったものの中でいくつかを具体的なコードと共にご紹介します。 今回具体的なコードを説明するものの他にも master で先に alias_method_chain を Module#prepend に変更 redirect_back を master で使えるように 等多くの backport を行なっています。

ApplicationRecord の変更

Rails5 からは各 model は ActiveRecord::Base ではなく ApplicationRecord を継承するようになりました。

その為 master でも 下記のように ApplicationRecord Class を定義し、各 model でそれを継承するように書き換えを行いました。

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

ActiveJob も同様に ApplicationJob を定義し、各 job でそれを継承するように書き換えました。

ActionController::TestRequest の変更

Rails5 からは ActionController::TestRequest の引数の変更によって controller spec にて post :create, user: { name: 'test' } と記述すると WARNING が発生するようになりました。

WARNING を発生させないようにするには post :create, params: { user: { name: 'test' } } と書く必要があります。 この変更を master でも有効にする為に下記のような処理を追加しました。

# spec/support/action_controller.rb
module TemplateAssertionsCustom
  def process(*args)
    if kwarg_request?(args)
      parameters, session, body, flash, format, xhr = args[2].values_at(:params, :session, :body, :flash, :format, :xhr)
      if xhr
        args[2].delete(:xhr)
        xml_http_request(args[1].downcase, args[0], args[2], nil, nil, false)
      else
        parameters ||= {}
        parameters.merge!(format: format) if format
        super(args[0], args[1], parameters, session, body, flash, format)
      end
    else
      non_kwarg_request_warning if args[2]
      super
    end
  end

  def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil, warning = true)
    if warning
      ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc, caller[6..1])
        xhr and xml_http_request methods are deprecated in favor of
        `get :index, xhr: true` and `post :create, xhr: true`
      MSG
    end
    super(request_method, action, parameters, session, flash)
  end

  private
  REQUEST_KWARGS = %i(params session flash body xhr)
  def kwarg_request?(args)
    args[2].respond_to?(:keys) && (
      (args[2].key?(:format) && args[2].keys.size == 1) ||
      args[2].keys.any? { |k| REQUEST_KWARGS.include?(k) }
    )
  end

  def non_kwarg_request_warning
    ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc, caller[6..-1])
      ActionController::TestCase HTTP request methods will accept only
      keyword arguments in future Rails versions.
      Examples:
      get :show, params: { id: 1 }, session: { user_id: 1 }
      process :update, method: :post, params: { id: 1 }
    MSG
  end
end

module ActionController::TemplateAssertions
  prepend TemplateAssertionsCustom
end

spec/spec_helper.rb にて上記のファイルを require することにより Rails4.2 でも Rails5 と同じ記述を行わなければ WARNING が発生するようになります。

ActionDispatch::Integration::Session の変更

こちらは ActionController::TestRequest の変更 と内容的には近いのですが、ActionDispatch::Integration::Session の引数の変更により request spec で get 'users.json', limit: 1 と記すると WARNING が発生するようになりました。

WARNING を発生させないようにするには get 'users.json', params: { limit: 1 } と書く必要があります。 この変更を master でも有効にする為に下記のような処理を追加しました。

# spec/support/action_dispatch.rb
module SessionCustom
  private
  def process(method, path, parameters = nil, headers_or_env = nil)
    parameters ||= {}
    if kwarg_request?(parameters)
      headers_or_env ||= {}
      headers_or_env.merge!(parameters[:headers] || parameters[:env] || {})
      super(method, path, parameters[:params], headers_or_env)
    else
      non_kwarg_request_warning if parameters.present?
      super
    end
  end

  REQUEST_KWARGS = %i(params headers env xhr as)
  def kwarg_request?(args)
    args.respond_to?(:keys) && args.keys.any? { |k| REQUEST_KWARGS.include?(k) }
  end

  def non_kwarg_request_warning
    ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc, caller[1..-1])
      ActionDispatch::IntegrationTest HTTP request methods will accept only
      the following keyword arguments in future Rails versions:
      #{REQUEST_KWARGS.join(', ')}
      Examples:
      get '/profile',
        params: { id: 1 },
        headers: { 'X-Extra-Header' => '123' },
        env: { 'action_dispatch.custom' => 'custom' },
        xhr: true,
        as: :json
    MSG
  end
end

module ActionDispatch
  module Integration
    class Session
      prepend SessionCustom
    end
  end
end

こちらも同じくspec/spec_helper.rb にて上記のファイルを require することにより Rails4.2 でも Rails5 と同じ記述を行わなければ WARNING が発生するようになります。

移行の際の注意

Rails4.2 から Rails5 への移行は session 周りの非互換などはないのでユーザへの影響は少なく抑えられるのですが、minne では ApplicationJob にてエラーが発生するパターンがあったのでその対策を行なってリリース作業を行いました。

minne では ApplicationJob 経由で sidekiq を使用して非同期処理の実装を行なっているのですが、Rails4.2 で ActionMailer を使用して job が積まれた際に ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MysqlDateTime という class がシリアライズされます、しかし Rails 5 ではこの class が存在しないのでデシリアライズする際にエラーが発生してしまいます。

Rails4.2 の server から積まれた job を Rails5 の worker が処理しようとするとエラーが発生するという状況になります。 その為一時的に下記のように class を定義することによってエラーが発生しないようにしました。

class ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MysqlDateTime < ActiveRecord::Type::DateTime
  private

  def has_precision?
    precision || 0
  end
end

まとめ

実際の移行はオンタイムで行いましたが、移行後はレスポンスタイムの悪化も許容範囲内でエラーも発生せず無事に Rails5 のアップグレードを行うことができました。

こういった改善はサービスを利用する方々には直接影響のないものですが、継続することこそがサービス全体の価値提供のスピードを保つことに繋がると考えています。

一部ではありますが minne の Rails を Rails5 へアップグレードした際に行なったことの紹介でした。