问题描述
我有一个 rake 任务,它循环遍历预订,并使用 .deliver
方法(我从 here 那里得到的)为每个人发送一封电子邮件(我有意识 现在 7 岁了).
# Select bookings starting soon
bookings = Booking.where('start_time < ?',24.hours.since)
# Email a reminder
bookings.each do |booking|
customer = booking.customer
CustomerMailer.reminder_24h(customer,booking).deliver
end
由于循环处于 rake 任务中,我认为调用 .deliver_later
没有任何价值,所以我只使用 .deliver
就像在 rails cast 中一样
我很想知道是否有有助于使用 Action Mailer 的最佳实践,例如每封电子邮件之间是否应该有一个 sleep 2
?或者我应该总是使用 .deliver_later
来减轻服务器上的负载?是否有任何其他与 Rails 相关的原因导致我的代码无法运行(或者,更糟糕的是,我使用了任何应该重构的反模式?)
TL;DR 为什么像上面的代码那样循环发送的电子邮件有时会发送失败
解决方法
没有答案,但来自其他论坛的一些建议。
发送电子邮件是一个充满潜在故障的过程。在后台作业中执行此操作始终是个好主意,该作业可以在出现网络等间歇性错误时重新尝试,也可以因地址错误而跳过。
以下是可能起作用的草图:
# Reminder process rake task
namespace :bookings do
desc "Deliver reminders to upcoming bookings"
task remind_upcoming: :environment do
EnqueueUpcomingBookingReminders.call(UpcomingBookingRemindersQuery.call)
end
end
class EnqueueUpcomingBookingReminders
def self.call(bookings_scope)
booking_communication_attrs =
bookings_scope
.pluck(:id)
.map { |id| {booking_id: id,type: "reminder"} }
communications_result =
BookingCommunication.insert_all(booking_communication_attrs,unique_by: %i[booking_id type])
# Email a reminder
communications_result.rows.flatten.each do |communication_id|
DeliverBookingCommunicationJob.perform_later(communication_id)
end
end
end
class UpcomingBookingRemindersQuery
def self.call(scope: Booking)
Booking
.upcoming_this_day
.left_outer_joins(:communications)
.merge(BookingCommunication.reminder)
.where(communications: {id: nil})
end
end
class Booking
has_many :communications,class_name: "BookingCommunication"
def self.upcoming_this_day
where(starts_at:,(Time.current..24.hours.from_now))
end
end
class BookingCommunication
belongs_to :booking
enum step: {confirmation: "confirmation",reminder: "reminder"} # combination of this and the booking id should be unique
enum status: {pending: "pending",delivered: "delivered",canceled: "canceled",failed: "failed"} # should default to pending at database layer
end
class DeliverBookingCommunicationJob < ApplicationJob
def perform(communication_id)
communication = BookingCommunication.find_by(communication_id)
# Guard against state that invalidates us running this job
return unless communication
return unless communication.pending?
return communication.canceled! if communication.booking.canceled? # This should probably live in the cancel booking process
booking = communication.booking
mailer = CustomerMailer.with(customer: booking.customer,booking: booking)
case communication.step
when "reminder"
mailer.reminder_24h.deliver_now
else
# log unknown communication step,send to error tracking but dont raise since we do not want job to run again
end
communication.delivered!
rescue SomeEmailRelatedError => err
communication.failed!
# deliver err to error tracking service
end
end