WARN: NameError: uninitialized constant DeliveryMethods when moving code to ActiveJob

WARN: NameError: uninitialized constant DeliveryMethods when moving code to ActiveJob

我有代码可以向我网站的成员发送邀请,可以通过电子邮件、实时通知或 One Signal 发送。在我将邀请移动到 ActiveJob 以使用 Sidekiq 和 Redis 在后台处理之前,代码在开发中运行良好。我这样做只是为了当组织的维护者上传联系人的 CSV 文件以邀请他们的组织。 (因此后台工作,因为一些客户希望邀请大约 10,000 多人,如果在控制器内完成,这会使系统陷入困境。)

如果我将任务移至 ActiveJob,我会在 Sidekiq 输出中收到此错误:

WARN: NameError: uninitialized constant DeliveryMethods

我认为这是因为我没有在 ActiveJob 中放置 require 语句,所以我将此添加到 ActiveJob 的顶部:

require 'application_notification'

但是,我得到了同样的错误信息。

我很茫然。任何帮助将不胜感激。这是代码片段。如果您还需要什么,请告诉我。

版本

Ruby:'3.0.2'

Rails: 7.0.0.alpha gem 'rails', :github => 'rails/rails', :branch => 'main'

Redis: '~> 4.1.3'

Sidekiq:“6.0.7”

结果输出

# Terminal Output

Started POST "/import_wizard/organization/1" for ::1 at 2021-08-10 16:47:30 -0700
Processing by InvitationsController#invite_imports as JS
  Parameters: {"authenticity_token"=>"--REDACTED--", "invitable_type"=>"organization", "invitable_id"=>"1"}
  Member Load (1.1ms)  SELECT "members".* FROM "members" WHERE "members"."id" =  ORDER BY "members"."id" ASC LIMIT   [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/concerns/cookies_concern.rb:171:in `load_cookies'
  Organization Load (1.3ms)  SELECT "organizations".* FROM "organizations" WHERE "organizations"."id" =  LIMIT   [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/invitations_controller.rb:216:in `set_invitable'
  ImportResult Load (0.8ms)  SELECT "import_results".* FROM "import_results" WHERE "import_results"."invitable_id" =  AND "import_results"."status" =  LIMIT   [["invitable_id", 1], ["status", 1], ["LIMIT", 1]]
  ↳ app/controllers/invitations_controller.rb:261:in `set_imports_to_invite'
  ImportRecord Load (0.7ms)  SELECT "import_records".* FROM "import_records" WHERE "import_records"."import_result_id" =  AND "import_records"."status" =   [["import_result_id", 32], ["status", "ready"]]
  ↳ app/controllers/invitations_controller.rb:155:in `invite_imports'
[ActiveJob] Enqueued InviteImportedMembersJob (Job ID: 69355585-cfef-4f1f-bf90-eae0f24d5f98) to Sidekiq(imports) with arguments: 
  #<GlobalID:0x00007fbeba81d0a0 @uri=#<URI::GID gid://prayer-nook/Organization/1>>,             
        [#<GlobalID:0x00007fbeba81c6a0 @uri=#<URI::GID gid://prayer-nook/ImportRecord/309>>, 
        #<GlobalID:0x00007fbeba817d08 @uri=#<URI::GID gid://prayer-nook/ImportRecord/310>>, 
        #<GlobalID:0x00007fbeba817470 @uri=#<URI::GID gid://prayer-nook/ImportRecord/311>>, 
        #<GlobalID:0x00007fbeba816d68 @uri=#<URI::GID gid://prayer-nook/ImportRecord/312>>, 
        #<GlobalID:0x00007fbeba816250 @uri=#<URI::GID gid://prayer-nook/ImportRecord/313>>, 
        #<GlobalID:0x00007fbeba8157b0 @uri=#<URI::GID gid://prayer-nook/ImportRecord/318>>, 
        #<GlobalID:0x00007fbeba814a40 @uri=#<URI::GID gid://prayer-nook/ImportRecord/319>>],
   #<GlobalID:0x00007fbeb9a5f198 @uri=#<URI::GID gid://prayer-nook/Member/1>>
  Rendering invitations/invite_imports.js.erb
  Rendered invitations/invite_imports.js.erb (Duration: 0.1ms | Allocations: 10)
Completed 200 OK in 317ms (Views: 3.4ms | ActiveRecord: 95.8ms | Allocations: 57969)

控制器操作

invite_imports_task 的注释行是我在控制器中使用与在 ActiveJob 中运行的代码完全相同但有效的方法。所以,我知道代码有效,它只是移动到现在导致问题的 ActiveJob。

# InvitationsController#invite_imports
# app/controllers/invitations_controller.rb

  def invite_imports
    set_invitable
    set_imports_to_invite
    @import_step = 4

    imports_to_invite_array = []
    @imports_to_invite.each do |record|
      imports_to_invite_array << record
    end

    InviteImportedMembersJob.perform_later(@invitable, imports_to_invite_array, @authenticated_member)

    # invite_imports_task(@invitable, imports_to_invite_array, @authenticated_member)

    respond_to do |format|
      format.js
    end
  end

有效工作

# app/jobs/invite_imported_members_job.rb

class InviteImportedMembersJob < ApplicationJob
  require 'application_notification'
  queue_as :imports

  def perform(invitable, imports_to_invite, sender)
    set_import_result(invitable)

    imported_emails = imports_to_invite.map {|member| member[:email]}
    member_list = Member.where(email: imported_emails)
    member_email_list = member_list.pluck(:email)
    non_member_email_list = imported_emails - member_email_list
    sent_invites = []
    error_in_sending_invites = []

    member_list.each do |member|
      invitation = Invitation.new(invitable: invitable, sender: sender, recipient:member)
      if invitation.save
        invitable.invited_members << member
        sent_invites << member.email
      else 
        error_in_sending_invites << member.email
      end
    end
    
    non_member_email_list.each do |member|
      InvitationMailer.with(recipient_email: member, sender: sender).app_invitation.deliver_later
      waitlist = InvitationWaitlist.create(email: member, invitable: invitable, sender: sender)

      # in this case the member variable is only an email address
      if waitlist.save
        sent_invites << member
      else
        error_in_sending_invites << member
      end
    end
      
    update_import_records(invitable, sent_invites, error_in_sending_invites)
    update_import_result
    create_cue_notification(invitable)
  end

  private
    def set_import_result(invitable)
      @import_result = ImportResult.find_by(invitable:invitable, status: 'waiting')
    end

    def update_import_records(invitable, sent_invites, error_in_sending_invites)
      if sent_invites.count > 0
        ImportRecord.where(import_result_id:@import_result.id, email: sent_invites).update_all(status:'sent')
      end

      if error_in_sending_invites.count > 0
        ImportRecord.where(import_result_id:@import_result.id, email: error_in_sending_invites).update_all(status:'error_in_sending')
      end
    end

    def update_import_result
      @import_result.completed!
    end

    def create_cue_notification(invitable)
      hide_old_cues(invitable)
      CueService.new(@import_result, set_cue_recipients(invitable), false).call!
    end

    def hide_old_cues(invitable)
      Cue.where(cueable: @import_result).update(status:'hidden')
    end

    def set_cue_recipients(invitable)
      if invitable.is_a?(Organization)
          return invitable.maintainers
      elsif invitable.is_a?(Group)
          return invitable.owner
      else
          return nil
      end
    end
end

申请通知

# app/notifications/application_notification.rb

class ApplicationNotification < Noticed::Base
  deliver_by :database, format: :format_for_database
  deliver_by :action_cable, channel: 'NotificationsChannel', format: :format_for_action_cable
  deliver_by :one_signal, class: "DeliveryMethods::OneSignal", format: :format_for_one_signal

  def format_for_database
    {
      type: self.class.name,
      params: params
    }
  end
end

DeliveryMethod::OneSignal

# app/notifications/delivery_methods/one_signal.rb

class DeliveryMethods::OneSignal < Noticed::DeliveryMethods::Base
  def deliver
    return unless app_id.present? && one_signal_url.present? && player_id.present?

    params = {"app_id" => app_id, 
      "contents" => {"en" => message},
      "headings" => {"en" => "Prayer Nook"},
      "include_player_ids" => [player_id],
      "data" => data
    }

    uri = URI.parse(one_signal_url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true

    request = Net::HTTP::Post.new(uri.path,'Content-Type'  => 'application/json;charset=utf-8')
    request.body = params.as_json.to_json
    response = http.request(request) 
    puts "OneSignal response: #{response.body}"
  end

  private

  def app_id
    ENV['ONE_SIGNAL_APP_ID']
  end

  def one_signal_url
    ENV['ONE_SIGNAL_API_URL']
  end

  def player_id
    recipient.site_profile.one_signal_id
  end

  def message
    if (method = options[:format])
      notification.send(method)[:message]
    else
      "Message from Prayer Nook"
    end
  end

  def data
    if (method = options[:format])
      notification.send(method)[:data]
    else
      { }
    end
  end
end

来自邀请模型

## app/models/invitation.rb

def send_notifications
    if self.invitable_type == 'Group'
      GroupInvitationNotification.with(invitation: self, group: self.invitable, sender: self.sender).deliver_later(self.recipient)
    elsif self.invitable_type == 'Organization'
      OrgInvitationNotification.with(invitation: self, organization: self.invitable, sender: self.sender).deliver_later(self.recipient)
    end
end

OrgInvitationNotification

# app/notifications/org_invitation_notification.rb

class OrgInvitationNotification < ApplicationNotification
  # this class inherits other delivery methods from ApplicationNotification: database, action_cable, and one_signal
  deliver_by :email, mailer: "InvitationMailer", method: :org_invitation, if: :immediate_email_notifications?

  # required params
  param :invitation
  param :organization
  param :sender

  # helper methods to make rendering easier.
  
  def format_for_action_cable 
    html = ApplicationController.render(
      partial: 'notifications/toast',
      locals: { header: "You've been invited",
                message: message,
                link_path: invitation_path(params[:invitation])
      }
    )
    params.merge(html: html)
  end

  def format_for_one_signal
    {
      message: message,
      data: { page: 'invitation', id: params[:invitation].id }
    }
  end

  def immediate_email_notifications?
    recipient.site_profile.invitations_email_notifications == 'immediately'
  end

  def message
    t(".message", sender: params[:sender].full_name, org_name: params[:organization].name)
  end
  
  def url
    invitation_url(params[:invitation])
  end
end

更新 根据@LamPhan 的评论新代码块:

# From config/application.rb

class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0

    config.active_job.queue_adapter = :sidekiq
    config.active_record.encryption.support_unencrypted_data = true
    config.active_record.legacy_connection_handling = false

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.

    config.generators do |g|
      g.test_framework :rspec,
      fixtures: false,
      view_specs: false,
      helper_specs: false,
      routing_specs: false
    end

    config.autoloader = :classic
end

根据评论,您正在使用 :classic 加载程序,并且您的项目 运行 在 Rails 7.0 上。

基于 this comment (of the creator of sidekiq)Sidekiq does not work with the classic autoloader in Rails 6 at all.

所以你应该使用 :zeitwerk 加载器。