Allow mailer classes to customize the deliver_later queue name

The `deliver_later_queue_name` is already configurable on ActionMailer::Base,
however the value is inherited by all subclasses. Use a class-inheritable
attribute instead, so that subclasses can override.

Refs:
- https://github.com/rails/rails/pull/18587#issuecomment-324975192
This commit is contained in:
Jeffrey Hardy 2023-02-09 17:14:52 -05:00
parent 26e21fc854
commit fa17c9ac99
15 changed files with 109 additions and 38 deletions

@ -1,3 +1,17 @@
* The `deliver_later_queue_name` used by the default mailer job can now be
configured on a per-mailer basis. Previously this was only configurable
for all mailers via `ActionMailer::Base`.
Example:
```ruby
class EventsMailer < ApplicationMailer
self.deliver_later_queue_name = :throttled_mailer
end
```
*Jeffrey Hardy*
* Email previews now include an expandable section to show all headers.
Headers like `Message-ID` for threading or email service provider specific

@ -53,6 +53,7 @@ module ActionMailer
autoload :TestHelper
autoload :MessageDelivery
autoload :MailDeliveryJob
autoload :QueuedDelivery
def self.eager_load!
super

@ -460,12 +460,14 @@ module ActionMailer
# * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with
# <tt>delivery_method :test</tt>. Most useful for unit and functional testing.
#
# * <tt>delivery_job</tt> - The job class used with <tt>deliver_later</tt>. Defaults to
# +ActionMailer::MailDeliveryJob+.
# * <tt>delivery_job</tt> - The job class used with <tt>deliver_later</tt>. Mailers can set this to use a
# custom delivery job. Defaults to +ActionMailer::MailDeliveryJob+.
#
# * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>.
# * <tt>deliver_later_queue_name</tt> - The queue name used by <tt>deliver_later</tt> with the default
# <tt>delivery_job</tt>. Mailers can set this to use a custom queue. Defaults to <tt>:mailers</tt>.
class Base < AbstractController::Base
include DeliveryMethods
include QueuedDelivery
include Rescuable
include Parameterized
include Previews
@ -487,7 +489,6 @@ class Base < AbstractController::Base
helper ActionMailer::MailHelper
class_attribute :delivery_job, default: ::ActionMailer::MailDeliveryJob
class_attribute :default_params, default: {
mime_version: "1.0",
charset: "UTF-8",

@ -12,7 +12,6 @@ module DeliveryMethods
# Do not make this inheritable, because we always want it to propagate
cattr_accessor :raise_delivery_errors, default: true
cattr_accessor :perform_deliveries, default: true
cattr_accessor :deliver_later_queue_name, default: :mailers
class_attribute :delivery_methods, default: {}.freeze
class_attribute :delivery_method, default: :smtp

@ -9,7 +9,10 @@ module ActionMailer
#
# Exceptions are rescued and handled by the mailer class.
class MailDeliveryJob < ActiveJob::Base # :nodoc:
queue_as { ActionMailer::Base.deliver_later_queue_name }
queue_as do
mailer_class = arguments.first.constantize
mailer_class.deliver_later_queue_name
end
rescue_from StandardError, with: :handle_exception_with_mailer_class

@ -62,9 +62,10 @@ def processed?
# * <tt>:queue</tt> - Enqueue the email on the specified queue
# * <tt>:priority</tt> - Enqueues the email with the specified priority
#
# By default, the email will be enqueued using <tt>ActionMailer::MailDeliveryJob</tt>. Each
# ActionMailer::Base class can specify the job to use by setting the class variable
# +delivery_job+.
# By default, the email will be enqueued using <tt>ActionMailer::MailDeliveryJob</tt> on
# the +:mailers+ queue. Mailer classes can customize the queue name used for the default
# job by assigning a +deliver_later_queue_name+ class variable, or provide a custom job
# by assigning a +delivery_job+. When a custom job is used, it controls the queue name.
#
# class AccountRegistrationMailer < ApplicationMailer
# self.delivery_job = RegistrationDeliveryJob
@ -88,9 +89,10 @@ def deliver_later!(options = {})
# * <tt>:queue</tt> - Enqueue the email on the specified queue.
# * <tt>:priority</tt> - Enqueues the email with the specified priority
#
# By default, the email will be enqueued using <tt>ActionMailer::MailDeliveryJob</tt>. Each
# ActionMailer::Base class can specify the job to use by setting the class variable
# +delivery_job+.
# By default, the email will be enqueued using <tt>ActionMailer::MailDeliveryJob</tt> on
# the +:mailers+ queue. Mailer classes can customize the queue name used for the default
# job by assigning a +deliver_later_queue_name+ class variable, or provide a custom job
# by assigning a +delivery_job+. When a custom job is used, it controls the queue name.
#
# class AccountRegistrationMailer < ApplicationMailer
# self.delivery_job = RegistrationDeliveryJob

@ -0,0 +1,12 @@
# frozen_string_literal: true
module ActionMailer
module QueuedDelivery
extend ActiveSupport::Concern
included do
class_attribute :delivery_job, default: ::ActionMailer::MailDeliveryJob
class_attribute :deliver_later_queue_name, default: :mailers
end
end
end

@ -170,11 +170,14 @@ def assert_enqueued_emails(number, &block)
# ContactMailer.with(email: 'user@example.com').welcome.deliver_later
# end
# end
def assert_enqueued_email_with(mailer, method, params: nil, args: nil, queue: ActionMailer::Base.deliver_later_queue_name || "default", &block)
def assert_enqueued_email_with(mailer, method, params: nil, args: nil, queue: nil, &block)
if mailer.is_a? ActionMailer::Parameterized::Mailer
params = mailer.instance_variable_get(:@params)
mailer = mailer.instance_variable_get(:@mailer)
end
queue ||= mailer.deliver_later_queue_name || ActiveJob::Base.default_queue_name
args = if args.is_a?(Hash)
[mailer.to_s, method.to_s, "deliver_now", params: args, args: []]
elsif params.present?
@ -182,6 +185,7 @@ def assert_enqueued_email_with(mailer, method, params: nil, args: nil, queue: Ac
else
[mailer.to_s, method.to_s, "deliver_now", args: Array(args)]
end
assert_enqueued_with(job: mailer.delivery_job, args: args, queue: queue.to_s, &block)
end

@ -5,6 +5,8 @@
class DelayedMailerError < StandardError; end
class DelayedMailer < ActionMailer::Base
self.deliver_later_queue_name = :delayed_mailers
cattr_accessor :last_error
cattr_accessor :last_rescue_from_instance

@ -10,10 +10,10 @@ class MessageDeliveryTest < ActiveSupport::TestCase
setup do
@previous_logger = ActiveJob::Base.logger
@previous_delivery_method = ActionMailer::Base.delivery_method
@previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name
ActionMailer::Base.deliver_later_queue_name = :test_queue
ActionMailer::Base.delivery_method = :test
ActiveJob::Base.logger = Logger.new(nil)
ActionMailer::Base.delivery_method = :test
ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
@ -28,7 +28,6 @@ class MessageDeliveryTest < ActiveSupport::TestCase
ActiveJob::Base.logger = @previous_logger
ActionMailer::Base.delivery_method = @previous_delivery_method
ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name
DelayedMailer.last_error = nil
DelayedMailer.last_rescue_from_instance = nil
@ -75,7 +74,7 @@ def test_should_enqueue_and_run_correctly_in_activejob
end
end
test "should enqueue a delivery with a delay" do
test "should enqueue delivery with a delay" do
travel_to Time.new(2004, 11, 24, 1, 4, 44) do
assert_performed_with(job: ActionMailer::MailDeliveryJob, at: Time.current + 10.minutes, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]]) do
@mail.deliver_later wait: 10.minutes
@ -83,25 +82,25 @@ def test_should_enqueue_and_run_correctly_in_activejob
end
end
test "should enqueue a delivery with a priority" do
test "should enqueue delivery with a priority" do
job = @mail.deliver_later priority: 10
assert_equal 10, job.priority
end
test "should enqueue a delivery at a specific time" do
test "should enqueue delivery at a specific time" do
later_time = Time.current + 1.hour
assert_performed_with(job: ActionMailer::MailDeliveryJob, at: later_time, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]]) do
@mail.deliver_later wait_until: later_time
end
end
test "should enqueue the job on the correct queue" do
assert_performed_with(job: ActionMailer::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]], queue: "test_queue") do
test "should enqueue delivery on the correct queue" do
assert_performed_with(job: ActionMailer::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]], queue: "delayed_mailers") do
@mail.deliver_later
end
end
test "should enqueue the job with the correct delivery job" do
test "should enqueue delivery with the correct job" do
old_delivery_job = DelayedMailer.delivery_job
DelayedMailer.delivery_job = DummyJob
@ -114,12 +113,26 @@ def test_should_enqueue_and_run_correctly_in_activejob
class DummyJob < ActionMailer::MailDeliveryJob; end
test "can override the queue when enqueuing mail" do
test "delivery queue can be overridden when enqueuing mail" do
assert_performed_with(job: ActionMailer::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]], queue: "another_queue") do
@mail.deliver_later(queue: :another_queue)
end
end
test "delivery queue can be overridden in subclasses" do
previous_queue_name = DelayedMailer.deliver_later_queue_name
DelayedMailer.deliver_later_queue_name = :throttled_mailers
assert_equal :throttled_mailers, DelayedMailer.deliver_later_queue_name
assert_equal :mailers, ActionMailer::Base.deliver_later_queue_name
assert_performed_with(job: ActionMailer::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: []], queue: "throttled_mailers") do
DelayedMailer.test_message.deliver_later
end
ensure
DelayedMailer.deliver_later_queue_name = previous_queue_name
end
test "deliver_later after accessing the message is disallowed" do
@mail.message # Load the message, which calls the mailer method.

@ -17,18 +17,13 @@ class DummyDeliveryJob < ActionMailer::MailDeliveryJob
@previous_delivery_method = ActionMailer::Base.delivery_method
ActionMailer::Base.delivery_method = :test
@previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name
ActionMailer::Base.deliver_later_queue_name = :test_queue
@mail = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com").invitation
end
teardown do
ActiveJob::Base.logger = @previous_logger
ParamsMailer.deliveries.clear
ActionMailer::Base.delivery_method = @previous_delivery_method
ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name
end
test "parameterized headers" do

@ -38,6 +38,10 @@ class CustomDeliveryMailer < TestHelperMailer
self.delivery_job = CustomDeliveryJob
end
class CustomQueueMailer < TestHelperMailer
self.deliver_later_queue_name = :custom_queue
end
class TestHelperMailerTest < ActionMailer::TestCase
include ActiveSupport::Testing::Stream
@ -371,6 +375,22 @@ def test_assert_enqueued_email_with_when_queue_arg_is_symbol
end
end
def test_assert_enqueued_email_with_when_mailer_has_custom_deliver_later_queue
assert_nothing_raised do
assert_enqueued_email_with CustomQueueMailer, :test do
silence_stream($stdout) do
CustomQueueMailer.test.deliver_later
end
end
assert_enqueued_email_with CustomQueueMailer, :test, queue: :custom_queue do
silence_stream($stdout) do
CustomQueueMailer.test.deliver_later
end
end
end
end
def test_assert_enqueued_email_with_when_mailer_has_custom_delivery_job
assert_nothing_raised do
assert_enqueued_email_with CustomDeliveryMailer, :test do

@ -818,7 +818,7 @@ files (environment.rb, production.rb, etc...)
|`perform_deliveries`|Determines whether deliveries are actually carried out when the `deliver` method is invoked on the Mail message. By default they are, but this can be turned off to help functional testing. If this value is `false`, `deliveries` array will not be populated even if `delivery_method` is `:test`.|
|`deliveries`|Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful for unit and functional testing.|
|`delivery_job`|The job class used with `deliver_later`. Defaults to `ActionMailer::MailDeliveryJob`.|
|`deliver_later_queue_name`|The name of the queue used with `deliver_later`.|
|`deliver_later_queue_name`|The name of the queue used with the default `delivery_job`. Defaults to `:mailers|
|`default_options`|Allows you to set default values for the `mail` method options (`:from`, `:reply_to`, etc.).|
For a complete writeup of possible configurations see the

@ -2112,9 +2112,18 @@ Enable or disable mailer previews. By default this is `true` in development.
config.action_mailer.show_previews = false
```
#### `config.action_mailer.perform_caching`
Specifies whether the mailer templates should perform fragment caching or not. If it's not specified, the default will be `true`.
#### `config.action_mailer.deliver_later_queue_name`
Specifies the Active Job queue to use for delivery jobs. When this option is set to `nil`, delivery jobs are sent to the default Active Job queue (see `config.active_job.default_queue_name`). Make sure that your Active Job adapter is also configured to process the specified queue, otherwise delivery jobs may be silently ignored.
Specifies the Active Job queue to use for the default delivery job (see `config.action_mailer.delivery_job`). When this option is set to `nil`, delivery jobs are sent to the default Active Job queue (see `config.active_job.default_queue_name`).
Mailer classes can override this to use a different queue. Note that this only applies when using the default delivery job. If your mailer is using a custom
job, its queue will be used.
Ensure that your Active Job adapter is also configured to process the specified queue, otherwise delivery jobs may be silently ignored.
The default value depends on the `config.load_defaults` target version:
@ -2123,10 +2132,6 @@ The default value depends on the `config.load_defaults` target version:
| (original) | `:mailers` |
| 6.1 | `nil` |
#### `config.action_mailer.perform_caching`
Specifies whether the mailer templates should perform fragment caching or not. If it's not specified, the default will be `true`.
#### `config.action_mailer.delivery_job`
Specifies delivery job for mail.

@ -1281,7 +1281,7 @@ def index
require "mail"
_ = ActionMailer::Base
assert_equal "test_default", ActionMailer::Base.class_variable_get(:@@deliver_later_queue_name)
assert_equal "test_default", ActionMailer::Base.deliver_later_queue_name
end
test "ActionMailer::DeliveryJob queue name is :mailers without the Rails defaults" do
@ -1292,7 +1292,7 @@ def index
require "mail"
_ = ActionMailer::Base
assert_equal :mailers, ActionMailer::Base.class_variable_get(:@@deliver_later_queue_name)
assert_equal :mailers, ActionMailer::Base.deliver_later_queue_name
end
test "ActionMailer::DeliveryJob queue name is nil by default in 6.1" do
@ -1304,7 +1304,7 @@ def index
require "mail"
_ = ActionMailer::Base
assert_nil ActionMailer::Base.class_variable_get(:@@deliver_later_queue_name)
assert_nil ActionMailer::Base.deliver_later_queue_name
end
test "valid timezone is setup correctly" do