【Rails】deviseとauditedを同時に使用するとstack level too deepが発生した件

既存プロジェクトにauditedを導入したところ、本番環境で大量のstack level too deepエラーが発生しました。
その原因と対処方法についてまとめます。

目次

プロジェクト環境

エラー発生時の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_membercurrent_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を導入されるプロジェクトはぜひ参考にしてみてください。

役に立ったらシェアしていただけると嬉しいです
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次