minne 事業部チーフテクニカルリードの @_shiro16 です。
おかげさまで minne は 1 月 17 日に 5 周年を迎えました。その 1 週間後の 1 月 24 日に Rails4.2 から 5 へのアップグレードを行いました。 そこで今回はアップグレードを行う際に行ったことの一部をご紹介します。
はじめに
今回は基本的なアップグレード手順の説明は省略し、minne ではどのように進めていったかを解説していきます。 アップグレード作業は主にチーフエンジニアの @hsbt と僕の 2 人で進めました。
まず minne がどの程度のコード量なのかをご覧いただこうと思います。
minne の rake stats
の結果は以下の通りです。
基本方針
- DEPRECATION WARNING は出来るだけ消す
- 可能なものは積極的に 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 へアップグレードした際に行なったことの紹介でした。