Serializza gli invii SMTP su una coda unica per non far chiudere Aruba.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
class Mailings::OutboundQueue
|
||||
LOCK_PATH = Rails.root.join("tmp/outbound_smtp.lock")
|
||||
STAMP_PATH = Rails.root.join("tmp/outbound_last_sent")
|
||||
|
||||
class << self
|
||||
attr_accessor :min_interval
|
||||
|
||||
def enqueue(recipient)
|
||||
recipient.with_lock do
|
||||
return false unless recipient.mailing.sending?
|
||||
return false unless recipient.pending?
|
||||
|
||||
recipient.update!(status: "queued", error_message: nil)
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def drain_one!
|
||||
File.open(lock_path, File::RDWR | File::CREAT, 0o644) do |file|
|
||||
return :busy unless file.flock(File::LOCK_EX | File::LOCK_NB)
|
||||
|
||||
wait = seconds_until_next_slot
|
||||
return :wait if wait.positive?
|
||||
|
||||
recipient = next_queued
|
||||
return :empty if recipient.nil?
|
||||
return :closed unless Mailings::SendClock.new(recipient.mailing).open?
|
||||
|
||||
begin
|
||||
CampaignMailer.raise_delivery_errors = true
|
||||
outcome = recipient.deliver_queued!
|
||||
stamp!
|
||||
outcome == :sent ? :sent : :deferred
|
||||
ensure
|
||||
CampaignMailer.raise_delivery_errors = false if Rails.env.development?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def exclusive
|
||||
File.open(lock_path, File::RDWR | File::CREAT, 0o644) do |file|
|
||||
file.flock(File::LOCK_EX)
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def next_queued
|
||||
queued_scope.order(:updated_at, :id).first
|
||||
end
|
||||
|
||||
def queued_scope
|
||||
MailingRecipient.queued.joins(:mailing).merge(Mailing.sending)
|
||||
end
|
||||
|
||||
def seconds_until_next_slot
|
||||
return 0 if min_interval.to_f <= 0
|
||||
|
||||
last = last_sent_at
|
||||
return 0 if last.nil?
|
||||
|
||||
remaining = min_interval - (Time.current - last)
|
||||
remaining.positive? ? remaining : 0
|
||||
end
|
||||
|
||||
def reset!
|
||||
@last_sent_at = nil
|
||||
File.delete(stamp_path) if File.exist?(stamp_path)
|
||||
rescue Errno::ENOENT
|
||||
nil
|
||||
end
|
||||
|
||||
def stamp!
|
||||
@last_sent_at = Time.current
|
||||
File.write(stamp_path, @last_sent_at.to_f.to_s)
|
||||
end
|
||||
|
||||
def last_sent_at
|
||||
return @last_sent_at if @last_sent_at
|
||||
return unless File.exist?(stamp_path)
|
||||
|
||||
@last_sent_at = Time.zone.at(Float(File.read(stamp_path)))
|
||||
rescue ArgumentError, TypeError, Errno::ENOENT
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def lock_path
|
||||
LOCK_PATH
|
||||
end
|
||||
|
||||
def stamp_path
|
||||
STAMP_PATH
|
||||
end
|
||||
end
|
||||
|
||||
self.min_interval = Rails.env.test? ? 0.0 : 180.0
|
||||
end
|
||||
@@ -0,0 +1,73 @@
|
||||
require "net/smtp"
|
||||
require "openssl"
|
||||
|
||||
class Mailings::SmtpGate
|
||||
MUTEX = Mutex.new
|
||||
RETRYABLE = [
|
||||
EOFError,
|
||||
IOError,
|
||||
Errno::ECONNRESET,
|
||||
Errno::EPIPE,
|
||||
Errno::ETIMEDOUT,
|
||||
Net::OpenTimeout,
|
||||
Net::ReadTimeout,
|
||||
OpenSSL::SSL::SSLError,
|
||||
Net::SMTPServerBusy
|
||||
].freeze
|
||||
|
||||
class << self
|
||||
attr_accessor :min_gap, :max_attempts, :backoff_base
|
||||
|
||||
def deliver
|
||||
MUTEX.synchronize { deliver_locked { yield } }
|
||||
end
|
||||
|
||||
def reset!
|
||||
MUTEX.synchronize { @last_monotonic = nil }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def deliver_locked
|
||||
attempts = 0
|
||||
begin
|
||||
wait_min_gap
|
||||
result = yield
|
||||
stamp!
|
||||
result
|
||||
rescue *RETRYABLE => e
|
||||
attempts += 1
|
||||
stamp!
|
||||
Rails.logger.warn("[smtp-gate] #{e.class}: #{e.message} attempt=#{attempts}/#{max_attempts}")
|
||||
raise if attempts >= max_attempts
|
||||
|
||||
sleep_wait(backoff_base * (2**(attempts - 1)))
|
||||
retry
|
||||
end
|
||||
end
|
||||
|
||||
def wait_min_gap
|
||||
return if min_gap.to_f <= 0 || @last_monotonic.nil?
|
||||
|
||||
elapsed = now - @last_monotonic
|
||||
remaining = min_gap - elapsed
|
||||
sleep_wait(remaining) if remaining.positive?
|
||||
end
|
||||
|
||||
def stamp!
|
||||
@last_monotonic = now
|
||||
end
|
||||
|
||||
def now
|
||||
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
end
|
||||
|
||||
def sleep_wait(seconds)
|
||||
sleep(seconds) if seconds.to_f.positive?
|
||||
end
|
||||
end
|
||||
|
||||
self.min_gap = 0.0
|
||||
self.max_attempts = 1
|
||||
self.backoff_base = 0.0
|
||||
end
|
||||
Reference in New Issue
Block a user