既存プロジェクトにauditedを導入したところ、本番環境で大量のstack level too deepエラーが発生しました。
その原因と対処方法についてまとめます。
プロジェクト環境
- rails 7.0.4
- devise 4.9.2
- audited 5.3.2 ※DBレコードの変更履歴を自動生成するgem
- devise_masquerade 2.1.2 ※代行ログインを実装するgem
エラー発生時のaudited設定
エラーが発生した時、auditedの設定は下記のようになっていました。
# config/initializers/audited.rb
Audited.current_user_method = :audited_update_by
# ApplicationController
class ApplicationController < ActionController::Base
def audited_update_by
if member_masquerade?
current_staff
else
if current_member.present?
current_member
else
current_staff
end
end
end
簡単にまとめると下記の仕様でcreated_byを設定するようにしていました。
1. 代行ログインで会員機能を使用している場合であれば、ログイン中のスタッフID
2. 会員機能使用中であれば会員ID
3. 管理機能使用中であればスタッフID
エラーログの内容
発生していたエラーは下記のようなエラーでした。10 occurrences in 5 minutes: SystemStackError: stack level too deep
「stack level too deep」はプログラムが無限ループに陥った際に発生するエラーです。
これは、メソッドが再帰的に呼び出され続けたり、オブジェクトの相互参照が原因で起こります。
backtraceにaudited_update_by
が何度も出現していたことから、無限ループはaudited_update_by
の中で発生していることが分かりました。
原因
原因はdeviseで動的に定義されるログイン中ユーザを管理するメソッド、
current_memberやcurrent_staffを、current_user_methodの中で使用していることにありました。
def current_#{mapping}
@current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
end
https://github.com/heartcombo/devise/blob/v4.9.2/lib/devise/controllers/helpers.rb#L125-L127
この際、認証を行う際に更新するカラムがauditedの管理対象となっていると、
audited_update_byを呼び出し → current_member呼び出し → 認証 →
auditedで記録を残すためaudited_update_by → current_member呼び出し → 認証 → ・・・
という永久ループが発生してしまいます。
対策
対策1 devise系のカラムをauditedの管理から外す
まずは認証前の状態でauditedの記録が発生しないように、deviseに関連して更新されるカラムを
auditedの記録の対象から外すことを試みました。
class Member < User
audited except: %i[ last_sign_in_at current_sign_in_at last_sign_in_ip current_sign_in_ip sign_in_count session_token confirmed_at reset_password_sent_at reset_password_token unconfirmed_email remember_created_at ]
・・・ですが、こちらの対応を行なってもstack level too deep問題は完全には解消しませんでした。
解消はしませんでしたが、sign in のたびに更新が走るカラムをauditedの対象にすると、
作成されるレコード数が大変なことになるので、devise系のカラムに限らず
できるだけ管理不要なカラムを対象外とした方が良いです。
対策2 current_#{mapping}ではなくインスタンス変数を使うよう修正する
current_member
、current_staff
を使用してしまうと、未認証な場合に認証が発生してしまいますが、@current_member
、@current_staff
を使用することで、未認証な場合でも認証が発生しなくなります。
未認証な場合はcreated_byがnullとして更新されます。
対策3 #{name}masquerade?を使用しない
これはdevise_masqueradeを使用しているプロジェクトについてですが、
current_user_methodの中ではmember_masquerade?
を使用しないよう修正する必要があります。
理由は対策2にも通じますが、member_masquerade?
の中では、current_member
を使用しているためです。
def #{name}masquerade?
return false if current#{name}.blank?
return false if session[#{name}_helper_session_key].blank?
if Devise.masquerade_storage_method_session?
session[#{name}_helper_session_key].present?
else
::Rails.cache.exist?(#{name}_helper_session_key).present?
end
end
https://github.com/oivoodoo/devise_masquerade/blob/v2.1.2/lib/devise_masquerade/controllers/helpers.rb#L42-L51
修正後のaudited設定
最終的に修正後のauditedの設定は下記の通りとなりました。
# config/initializers/audited.rb
Audited.current_user_method = :audited_update_by
# ApplicationController
class ApplicationController < ActionController::Base
def audited_update_by
@current_staff.present? ? @current_staff : @current_member
end
〜
deviseを使用されているプロジェクトは多いかと思います。
これからauditedを導入されるプロジェクトはぜひ参考にしてみてください。
コメント