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

この記事について

本記事は、2023年8月に社内で実施した勉強会の内容を基に、社外向けに再編集したものです。
記載の内容は執筆当時の情報であり、現在の仕様やベストプラクティスと異なる可能性があります。
実装にあたっては、必ず最新の公式ドキュメントをご確認いただくようお願いいたします。

はじめに

あるプロジェクトでauditedを導入した際、本番環境で大量の「stack level too deep」エラーが発生しました。
原因の特定には時間を要しましたが、本記事ではその原因の概要と、実際に行った対処方法をまとめています。

RubyGems.org – audited
https://rubygems.org/gems/audited/versions/4.2.1

auditedは、モデルの変更を検知して変更履歴を自動で記録するRubyのgemのこと。

auditedの導入

1. gemファイルへの追加

gem "audited"

2. auditedテーブルの作成

$ rails generate audited:install
$ rake db:migrate

3. 対象のmodelsにauditedを設定

class User < ApplicationRecord
  audited
  〜
end

4. 現在のユーザの設定

# config/initializers/audited.rbに下記を記述
Audited.current_user_method = :authenticated_user

# ApplicationControllerに現在のユーザを判定するメソッドを設定
class ApplicationController < ActionController::Base
  def authenticated_user
    if current_user
      current_user
    else
      'Alexander Fleming'
    end
  end
end
目次

プロジェクトでの導入初期時点での実装例

# 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
    〜

対策1:devise系のカラムをauditの管理から外す

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** last_accessed_on session_token confirmed_at 
      reset_password_sent_at reset_password_token unconfirmed_email
	    remember_created_at notify_count
	  ], if: :must_be_audited?

  def must_be_audited?
    # updated_by以外の更新対象が存在すること
    # audited_options[:except]は上でaudited except:に指定している内容、 Audited.ignored_attributesはModule側で指定されているデフォルトの値(updated_atなど)
    changed_attribute_names_to_save.empty? || (changed_attribute_names_to_save - (%w[updated_by] | audited_options[:except] | Audited.ignored_attributes)).present?
  end

sign inのたびに更新が発生するカラムをauditedの対象とすると、不要なレコードが大量に作成されてしまいます。
そのため、devise系カラムに限らず、不要なカラムは極力外すようにしています。

対策2:member_masquerade?を使用しない形に修正する

# 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
    〜

以下のようなエラーログが出力されていたことから、member_masquerade?メソッドが原因である可能性を疑いました。

app/controllers/base_controller.rb:75:in `member_masquerade?'
app/controllers/application_controller.rb:47:in `audited_update_by'

後述する対策3にも通ずる部分となるのですが、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/ec616953226cbd426b7e0d9083ab36dcf89e363e/lib/devise_masquerade/controllers/helpers.rb#L42-L51

対策3:current_membercurrent_staffではなくインスタンス変数を使うよう修正する

# 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
    〜

current_memberを使用することで、warden.authenticateが呼び出され、結果としてメンバー情報の更新 → auditedによる監視 → 再びcurrent_memberの参照 → warden.authenticate呼び出し、という循環が発生していたと考えられます。

def current_#{mapping}
  @current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
end
役に立ったらシェアしていただけると嬉しいです
  • URLをコピーしました!
  • URLをコピーしました!
目次