【しくじり先生】 RailsのAutoloadingとReloadingの仕組みとやってしまったバグ
by
Yuta Fujii
×
Copy
Open
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Slide 1
Slide 1 text
【しくじり先生】 RailsのAutoloadingとReloadingの仕組み とやってしまったバグ 2021.08.27
Slide 2
Slide 2 text
@__yutafujii__ ● 定数のAutoloadingとReloadingの仕組み ● 開発環境でのサーバー処理 ● しくじりました話 Contents
Slide 3
Slide 3 text
@__yutafujii__ ● HR領域に関連するWebシステム ● 2019年に入社して開発・運用を行っている ● 当時のRailsのバージョンは5.2 ● しばらくしてフロントをVueで書くようになった 前提知識
Slide 4
Slide 4 text
class Dog < Animal
Slide 5
Slide 5 text
いきなりですが
Slide 6
Slide 6 text
@__yutafujii__ このRubyコードを実行すると...
Slide 7
Slide 7 text
@__yutafujii__ →知らない定数があるとエラーが出る
Slide 8
Slide 8 text
@__yutafujii__ 事前にそれら定数が書かれているファイルをrequireする
Slide 9
Slide 9 text
@__yutafujii__ ● このコードはなぜ動くのか なぜRailsはrequireを書かなくていいのか?
Slide 10
Slide 10 text
もうひとつ
Slide 11
Slide 11 text
@__yutafujii__ なぜRailsはファイル修正したらすぐ反映されるのか?
Slide 12
Slide 12 text
@__yutafujii__ 今日はこの話を... https://railsguides.jp/autoloading_and_reloading_constants.html
Slide 13
Slide 13 text
Zeitwerk Classic →
Slide 14
Slide 14 text
@__yutafujii__ Autoloading / Reloadingの方法は過渡期にある Classic Classic Zeitwerk Zeitwerk <= 5.2 6.0 / 6.1 7.0 (deprecated)
Slide 15
Slide 15 text
Zeitwerk
Slide 16
Slide 16 text
ZeitwerkモードのAutoloading
Slide 17
Slide 17 text
loader = Zeitwerk::Loader.new loader.push_dir(...) # ...にautoload_paths loader.setup # ready!
Slide 18
Slide 18 text
def define(parent, cname, abspath) parent.autoload(cname, abspath) cref = [parent, cname] c2a[cref] = abspath a2c[abspath] = cref end
Slide 19
Slide 19 text
No content
Slide 20
Slide 20 text
[Object, :User] => "/Users/fxn/blog/app/models/user.rb" [Object, :Hotel] => "/Users/fxn/blog/app/models/hotel"
Slide 21
Slide 21 text
@__yutafujii__ 最初にディレクトリを探索して 定数と定義ファイルをHashで保持しているから なぜRailsはrequireを書かなくていいのか? Answer
Slide 22
Slide 22 text
ZeitwerkモードのReloading
Slide 23
Slide 23 text
ActiveSupport::FileUpdateChecker
Slide 24
Slide 24 text
Rails::Application::Finisher
Slide 25
Slide 25 text
autoloaders.main.reload # Zeitwerk::Loaderインスタンス
Slide 26
Slide 26 text
def unload_autoload(parent, cname) parent.__send__(:remove_const, cname) end
Slide 27
Slide 27 text
@__yutafujii__ Answer なぜRailsはファイル修正したらすぐ反映されるのか? ファイル修正を検知した場合, 前述のHashテーブルを再更新
Slide 28
Slide 28 text
ちなみにRails 5.2までは Classic こ う だ っ た
Slide 29
Slide 29 text
@__yutafujii__ 未知の定数に出会った時, それがありそうなファイルパスを探して読み込むから Autoloading Answer 実際には autoload_path に入っているパス Classic
Slide 30
Slide 30 text
@__yutafujii__ Answer rails server ファイル修正を検知した場合, 修正が加わったクラスやモジュールを定数リストから落とし 次のリクエスト受理時にAutoloadingを再度走らせるから Reloading Classic
Slide 31
Slide 31 text
@__yutafujii__ RailsはAutoloadingとReloadingのために何をしたのか? 未知の定数に遭遇 → NameError まず推論 ファイル修正 → 反映されない 必要に応じてリロード Autoloading Reloading Classic
Slide 32
Slide 32 text
@__yutafujii__ RailsはAutoloadingとReloadingのために何をしたのか? 未知の定数に遭遇 → NameError まず推論 ファイル修正 → 反映されない 必要に応じてリロード ● 既にある処理を変えている:メソッドのオーバーライト ○ Moduleクラスのconst_missing Autoloading Classic
Slide 33
Slide 33 text
@__yutafujii__ RailsはAutoloadingとReloadingのために何をしたのか? ActiveSupport::Dependencies Classic
Slide 34
Slide 34 text
@__yutafujii__ RailsはAutoloadingとReloadingのために何をしたのか? 未知の定数に遭遇 → NameError まず推論 ファイル修正 → 反映されない 必要に応じてリロード ● これまでにない処理がある:メソッドの追加 ○ consoleでのreload!メソッド ○ serverでのreload & autoloadのメカニズム(後述) Reloading Classic
Slide 35
Slide 35 text
@__yutafujii__ RailsはAutoloadingとReloadingのために何をしたのか? ActiveSupport::FileUpdateChecker Classic
Slide 36
Slide 36 text
@__yutafujii__ RailsはAutoloadingとReloadingのために何をしたのか? Rails::Application::Finisher Classic
Slide 37
Slide 37 text
開発環境でのサーバーの挙動
Slide 38
Slide 38 text
@__yutafujii__ 開発環境サーバーのレスポンス 受理 HTTP POST /admin/books_tags
Slide 39
Slide 39 text
@__yutafujii__ 開発環境サーバーのレスポンス 受理 routing namespace :admin do resources :books_tags end
Slide 40
Slide 40 text
@__yutafujii__ 開発環境サーバーのレスポンス 受理 routing controller 特定 controller_name = ‘admin/books_tags_controller’ controller_name.constantize Classic
Slide 41
Slide 41 text
@__yutafujii__ 開発環境サーバーのレスポンス 受理 routing controller 特定 controller_name = ‘admin/books_tags_controller’ controller_name.constantize # => Admin::BooksTagsController const_missing発火 Classic
Slide 42
Slide 42 text
@__yutafujii__ 開発環境サーバーのレスポンス 受理 routing controller 特定 処理 module Admin class BooksTagsController def create @tagging = BookTag.new # … end end end Classic
Slide 43
Slide 43 text
@__yutafujii__ 開発環境サーバーのレスポンス 受理 routing controller 特定 処理 module Admin class BooksTagsController def create @tagging = BookTag.new # … end end end const_missing発火 Classic
Slide 44
Slide 44 text
@__yutafujii__ 開発環境サーバーのレスポンス 受理 routing controller 特定 処理 レスポンス 200 OK POST /admin/books_tags
Slide 45
Slide 45 text
作業してファイル修正すると...
Slide 46
Slide 46 text
@__yutafujii__ ファイルに変更が加わっているとき 受理 routing controller 特定 処理 レスポンス 修正ファイルをunload unload対象の定数もリストから削除 Classic
Slide 47
Slide 47 text
@__yutafujii__ ファイルに変更が加わっているとき 受理 routing controller 特定 処理 レスポンス const_missing const_missing Autoloading Autoloading Classic
Slide 48
Slide 48 text
@__yutafujii__ Answer Reloadingの仕組み Classic
Slide 49
Slide 49 text
設定ミスによりAPI開発で しくじってしまった...
Slide 50
Slide 50 text
ある日,普通に開発していると...
Slide 51
Slide 51 text
@__yutafujii__ 突如現れるエラー A copy of XXXXXX has been removed from the module tree....
Slide 52
Slide 52 text
@__yutafujii__ 対処方法として見つかったもの① https://tech.unifa-e.com/entry/2017/08/09/183519 ググって見つかったブログより抜粋
Slide 53
Slide 53 text
@__yutafujii__ すると,次第に「なぜそこで起きている?!」という場所でこのエラーが登場するように なる レベルアップしていくエラー Api::One::Parent::ChildController
Slide 54
Slide 54 text
@__yutafujii__ 対処方法として見つかったもの② http://sugilog.hatenablog.com/entry/20110806/1312584149 ググって見つかったブログより抜粋 config.cache_class = true
Slide 55
Slide 55 text
@__yutafujii__ 対処方法として見つかったもの② config/development.rb
Slide 56
Slide 56 text
@__yutafujii__ この設定をすると,ソースコードを書き換えた時に変更が反映されなくなる Reloadingが効かない しかし
Slide 57
Slide 57 text
本当の原因
Slide 58
Slide 58 text
@__yutafujii__ 本当の原因 config.reload_classes_only_on_change = false
Slide 59
Slide 59 text
@__yutafujii__ 本当の原因
Slide 60
Slide 60 text
@__yutafujii__ 本当の原因 config.reload_classes_only_on_change = false の場合 リスエスト処理が完了するたびに 次のリクエストで都度定数を全削除してしまう
Slide 61
Slide 61 text
@__yutafujii__ リクエストの都度Reloadingされる 受理 routing controller 特定 処理 レスポンス const_missing Autoloading Classic remove_const
Slide 62
Slide 62 text
その結果, リクエストが同時に飛んでくると
Slide 63
Slide 63 text
@__yutafujii__ リクエストが同時に飛んでくると 受理 routing controller 特定 処理 レスポンス const_missing Autoloading(1) Classic remove_const(1) 受理 routing controller 特定 処理 レスポンス const_missing Autoloading(2) remove_const(2)
Slide 64
Slide 64 text
@__yutafujii__ 実際に起きていたこと GET /api/books GET /api/tags Vueインスタンス created() controller 特定 controller 特定 Subdomain::Api::BooksController Subdomain::Api::TagsController
Slide 65
Slide 65 text
@__yutafujii__ 実際に起きていたこと GET /api/books GET /api/tags Vueインスタンス created() Subdomain::Api::BooksController Subdomain::Api::TagsController Race Conditionの発生 controller 特定 controller 特定
Slide 66
Slide 66 text
片方が Subdomain Subdomain::Api Subdomain::Api::BooksController を読み込む間にもう片方が remove_const する
Slide 67
Slide 67 text
同一Class/Moduleのオブジェクトが 2個できてしまった Subdomain::Api
Slide 68
Slide 68 text
@__yutafujii__ 実際に起きていたこと
Slide 69
Slide 69 text
@__yutafujii__ Inflector.constantize(from_mod_name).equal?(from_mod) 実際に起きていたこと
Slide 70
Slide 70 text
全ては3年前に始まっていた
Slide 71
Slide 71 text
@__yutafujii__ 3年前の2018年3月に追加されていた
Slide 72
Slide 72 text
ところでこのrace conditionって
Slide 73
Slide 73 text
@__yutafujii__ イシューは上がっていたがそれ自体は解決されなかった https://github.com/rails/rails/issues/33209
Slide 74
Slide 74 text
@__yutafujii__ イシューは上がっていたがそれ自体は解決されなかった https://github.com/rails/rails/issues/33209
Slide 75
Slide 75 text
@__yutafujii__ ● 新たなautoloadの方法 登場するZeitwerk https://github.com/fxn/zeitwerk
Slide 76
Slide 76 text
@__yutafujii__ Classicモードはいずれ使えなくなる ● Zeitwerkモードに早めに移行しておこう https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#classic-mode-is-deprecated
Slide 77
Slide 77 text
@__yutafujii__ 移行方法 ● 実際にはテストが大量にこけたりしたが無事に移行済み https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#enabling-zeitwerk-mode
Slide 78
Slide 78 text
@__yutafujii__ Classicモードはいずれ使えなくなる https://github.com/rails/rails/commit/0d523d83657ce7066f25d87f6f094e804590e1e8ants.html#classic-mode-is-deprecated
Slide 79
Slide 79 text
85 828 → ActiveSupport::Dependencies
Slide 80
Slide 80 text
ありがとうございました
Slide 81
Slide 81 text
余談
Slide 82
Slide 82 text
@__yutafujii__ 本当の原因
Slide 83
Slide 83 text
@__yutafujii__ Application(選考)というModelを使っていたため Answer この設定が必要だった背景(推測)
Slide 84
Slide 84 text
@__yutafujii__ どうしても「選考」というデータが重要であり,自然な英訳で「Application」というモデル が作成された しかし,Applicationという名前のクラスはRailsのアプリケーション作成時に一つ作成さ れる HRに関するシステムだったので
Slide 85
Slide 85 text
@__yutafujii__ この設定が必要だった背景(推測) config/application.rb
Slide 86
Slide 86 text
@__yutafujii__ config/application.rbとapp/models/application.rb ソースコードでApplicationが使われている箇所では,app/models/application.rbをうま くautoloadしてくれなかった
Slide 87
Slide 87 text
@__yutafujii__ そうしたわけで,こうした設定が追加されていた
Slide 88
Slide 88 text
@__yutafujii__ requireしてしまうとreloadされない Never be require d https://guides.rubyonrails.org/autoloading_and_reloading_constants_classic_mode.html#autoloading-and-require
Slide 89
Slide 89 text
@__yutafujii__ だからこの設定がさらに追加されたのだろう
Slide 90
Slide 90 text
@__yutafujii__ その結果
Slide 91
Slide 91 text
@__yutafujii__ ● 定数のAutoloading/Reloadingは変わった ● Classicモードだったら早めに移行しておこう ● ソースコードの勉強になった ● きっと誰もしくじらないだろう まとめ
Slide 92
Slide 92 text
@__yutafujii__ 参考 RAILS GUIDES https://guides.rubyonrails.org/autoloading_and_reloading_constants_classic_mode.html https://guides.rubyonrails.org/autoloading_and_reloading_constants.html Rails GitHub https://github.com/rails/rails/blob/main/activesupport/lib/active_support/dependencies.rb https://github.com/rails/rails/blob/main/activesupport/lib/active_support/inflector/methods.rb Rails Issue https://github.com/rails/rails/issues/33209 Zeitwerk GitHub https://github.com/fxn/zeitwerk#pronunciation バグ検証メモ https://zenn.dev/yutafujii/scraps/cd5500cd468a39
Slide 93
Slide 93 text
ありがとうございました