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

ありがとうございました