【しくじり先生】 RailsのAutoloadingとReloadingの仕組みとやってしまったバグ
by
Yuta Fujii
Link
Embed
Share
Beginning
This slide
Copy link URL
Copy link URL
Copy iframe embed code
Copy iframe embed code
Copy javascript embed code
Copy javascript embed code
Share
Tweet
Share
Tweet
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
ありがとうございました