SmartHR Tech Blog

スマートエイチアール開発者ブログ

Rails6でbyebugを利用してデバッグするときに気をつけたいこと

SmartHRでRails顧問業をしていますwillnetです。最近はよくテストコードの治安維持活動をしています。

SmartHRでは複数のRailsアプリケーションが稼働していますが、その大半はRails6で動いています。先日、そのうちの一つのアプリケーションで「定義しているはずの定数がなぜか読み込めない」という奇妙な現象に遭遇したので原因を調査しました。

調査の結果、これはRails6を利用しているひとであればそれなりにハマる可能性がありそうだ、と感じたので今回ブログエントリにして周知しておこうと思った次第です。

定義しているはずの定数がなぜか読み込めない、とは

  • binding.pry時にA::Bのようにネームスペース配下の定数を参照したときNameError: uninitialized constant A::Bのようになることがある
  • class Aclass A::Bもアプリケーションコード中で確かに定義しているし、パスもapp/models/a.rb, app/models/a/b.rbのようになっていて問題ないように見える

簡単にまとめるとこういうことです

zeitwerkで管理しているプロジェクトでbyebugpry-byebugのブレークポイントを利用しているとき、ネームスペースを利用した定数の解決がうまくいきません。

もっと詳しく

Rails6では、デフォルトのコードローダーとしてzeitwerkというものを利用しています。コードローダーをざっくり説明すると「明示的にrequireを書かずともいい感じに必要なファイルをrequireする」という定数の自動読み込み機能を提供するものです。

Rails5以前は、const_missingを利用して定数の自動読み込みを実現していましたが、このやり方では期待通りに動かないケースがあり、かつconst_missingの仕組み上改善が難しい、という問題がありました。

zeitwerkはconst_missingを使わず、autoloadTracePointを利用することでRails5以前のやりかたで起きる問題を解決しています*1

そんなzeitwerk(Rails6)の環境でapp/models/a.rbapp/models/a/b.rbが存在しているときにA::Bとすると、次の手順で動作します。

  1. Aをrequireする(Aのautoloadは起動時に設定済み)
  2. class Aを読み込んだタイミングで、 app/models/a/ (app/controllers/a/などがあればそれも)ディレクトリ配下にどのようなファイルが存在しているか調べ、それぞれのファイルに対応したautoloadを実行する
  3. A.autoload :B, '/path/to/railsapp/app/models/a/b.rb' が実行される
  4. A::Bがrequireされる

このとき2の「 class Aを読み込んだタイミング」を検知するのにTracePointが使われています。TracePointはRubyの様々なイベントに対してhookを仕掛けることのできる機能です。

ここで問題になるのが、次の2つの仕様です

  • byebugがブレークポイントを実装するのにTracePointを利用している
  • 現状のTracePointの仕様として「TracePointでhookした処理の中ではTracePointのhookが効かない」というものがある
    • おそらく、これを許容してしまうと意図せず無限にhookが効いてしまうのでそうしているのではないかと思います。

というわけで、byebugのブレークポイント中ではTracePointが効かないため上記の2が動かず、結果としてA::Bを自動読み込みできません。

再現できるように簡単なサンプルアプリケーションを作ったので、気になる方は↓をcloneして動かしてみてください。ブレークポイント中ではA::BがNameErrorになりますが、ブレークポイント外でA::Bとするとエラーにならない、というのが確認できます。

willnet/byebug-zeitwerk-sample

どうしたらいいの

現状では回避策はなさそうです。このような不具合があるというのを認識しつつ過ごすか、一旦byebugを使わないようにするか。byebugの中の人がIssueをたてているので、これがいい感じに着地するのを待ちましょう。

Feature #15912: Allow some reentrancy during TracePoint events - Ruby master - Ruby Issue Tracking System

*1:zeitwerkの詳細についてはまた別の機会に書いたりするかもしれません