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
加载器。
我有代码可以向我网站的成员发送邀请,可以通过电子邮件、实时通知或 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
加载器。