エラーの調査をしていたら知らない機能に出会った話
今回の記事はグリモアでサーバーサイドエンジニアとして働くたけおが担当します。
自己紹介
ゲームのAPIサーバー実装、運用のための管理画面の実装、サーバー管理など、サーバーに関わること全般を担当しています。
去年はSlack App についての記事を書きました。
経緯
「consoleでreload!するとエラー出るんですよね」
きっかけはサーバーサイドエンジニアの定例ミーティングでのひとことでした。
なぜそんなことが起こるのか興味が湧いたので、お隣のプロジェクトに首を突っ込んで見ることにしました。
調査開始
rails consoleでreload!してみると確かに再現します。
[1] pry(main)> reload!
Reloading...
NoMethodError: undefined method `end_line=' for nil:NilClass
node.end_line = @end_line
^^^^^^^^^^^^^
from /usr/local/lib/ruby/3.1.0/psych/tree_builder.rb:133:in `set_end_location'
これだけだと何が原因なのかよくわからないのでバックトレースを見てみます。
pryでは`wtf?`を使うと直近の例外のバックトレースを表示できます。
(いつも忘れるので毎回`help`で確認してます)
[2] pry(main)> wtf?
Exception: NoMethodError: undefined method `end_line=' for nil:NilClass
node.end_line = @end_line
^^^^^^^^^^^^^
--
0: /usr/local/lib/ruby/3.1.0/psych/tree_builder.rb:133:in `set_end_location'
1: /usr/local/lib/ruby/3.1.0/psych/tree_builder.rb:92:in `end_stream'
2: /usr/local/lib/ruby/3.1.0/psych/visitors/yaml_tree.rb:93:in `finish'
3: /usr/local/lib/ruby/3.1.0/psych/visitors/yaml_tree.rb:99:in `tree'
4: /usr/local/lib/ruby/3.1.0/psych.rb:600:in `dump_stream'
5: /usr/local/lib/ruby/3.1.0/psych/y.rb:6:in `y'
6: /srv/spec/factories/hoge_factory.rb:4:in `block (2 levels) in <main>'
7: /usr/local/bundle/gems/factory_bot-6.2.0/lib/factory_bot/syntax/default.rb:18:in `instance_eval'
8: /usr/local/bundle/gems/factory_bot-6.2.0/lib/factory_bot/syntax/default.rb:18:in `factory'
9: /srv/spec/factories/hoge_factory.rb:2:in `block in <main>'
どうやらhoge_factoryが怪しそうなので見てみます。
hoge_factoryの中身は以下のような感じで、座標を格納するxとyの2つのカラムが存在しています。
(※プロダクトコードをそのまま出せないのでちょっと改変しています)
FactoryBot.define do
factory :hoge do
x { 1 }
y { 1 }
end
end
バックトレースを見る限り、このyがpsychのメソッド呼び出しになってそうな雰囲気です。
psychの中へ
psych/yを確認してみます。
https://github.com/ruby/psych/blob/v4.0.4/lib/psych/y.rb#L2-L9
module Kernel
###
# An alias for Psych.dump_stream meant to be used with IRB.
def y *objects
puts Psych.dump_stream(*objects)
end
private :y
end
Kernelモジュールにyというメソッドを追加していて、引数で渡したオブジェクトをYAMLとして出力してくれるようです。
こんなメソッドがあったんですね。
KernelはObjectのancestorsに含まれているので、これをrequireしたらほぼどこでも使えるようになります。
[3] pry(main)> Object.ancestors
=> [ActiveSupport::Dependencies::RequireDependency, ActiveSupport::ToJsonWithActiveSupportEncoder, Object, PP::ObjectMixin, ActiveSupport::Tryable, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject]
psych/yをrequireしている箇所を探してみるとpsych/core_ext.rbにあり、`IRB`が定義されていたらyをrequireするようになっています。
if defined?(::IRB)
require_relative 'y'
end
つまり、rails consoleでyamlを使うと、yメソッドがKernelに追加されることになります。
factory_botの中も見てみる
factory_botのほうも実装を追いかけてみます。
問題となっているのはfactoryメソッドのブロックで、このブロックはDefinitionProxyオブジェクトに対してinstance_evalするときに渡されています。
つまり、hoge_factoryのxとかyはDefinitionProxyオブジェクトに対するメソッド呼び出しになりますが、そのようなメソッドは存在しないのでmethod_missingで処理することになります。
ですが、上で書いたようにKernelにyメソッドが追加されてしまうと、method_missingが呼ばれないので意図したとおりに動かなくなってしまいます。
https://github.com/thoughtbot/factory_bot/blob/v6.2.0/lib/factory_bot/definition_proxy.rb#L18-L20
このあたりを見ると余計なメソッドをundefする処理が入っていますが、実行順序の問題なのかうまくいってないようです。
対応方法の検討
エラーの理由がわかったところで、対応を検討します。
gemへのフィードバックができれば理想的ですが、とりあえず問題の解消を優先することにしました。
案1. factory内でyを使わないようにする
FactoryBot.define do
factory :hoge do
x { 1 }
add_attribute(:y) { 1 }
end
end
add_attributeを使えばmethod_missingせずに済みますが、
違う書き方が混在するのに違和感がある
yというカラムを持つテーブルが新たに増える可能性を否定できず、その場合にadd_attributeを使うように強制できない
という2つの理由から不採用となりました。
案2. consoleでyメソッドを無効にする
今回採用したのはこちらの方法です。
`console do ~ end`を使えばconsole向けの設定を書くことができるので、config/development.rbに以下のように追記し、yを未定義にしました。
console do
# reload!したときにfactory内にyがあるとpsychのyメソッドが呼び出されてしまうのでyを未定義にする
require "psych/y"
Kernel.module_eval do
undef :y
end
end
ちょっと荒っぽい対応のような気がしますが、一旦これで問題は解消しました。
次のアクション
とりあえずは問題を回避しましたが、どうもスッキリしないのできちんと根本解決したく、factory_botでundefできていないところをとっかかりに調べていこうかと考えています。
最後に
ここまでお付き合いいただき、本当にありがとうございます!
ということで、グリモアは一緒に【中二病を救う】側になってくれる仲間を大大大募集中です。
少しでもグリモアに興味をお持ちいただけましたら、是非とも下記の採用サイトをご覧ください。
この記事が参加している募集
読んでくださりありがとうございま――…… え?さぽーと…?いやいやいや!そんな恐れ多いですよ!でも、サポートいただけると、ゲーム開発が少しだけ楽になるかも…… あ!ごめんなさい、独り言ですっ!えへへへ……