From dc7fd821dca0b2088bde7e0a2a06bfe59e1ba5c6 Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Sat, 23 Jun 2012 14:36:09 -0400 Subject: Asynchronous ActionMailer Any ActionMailer class can be set to render and delier messages using the new Rails Queue. Some of this work was borrowed (with permission) from Nick Plante's (zapnap) reqsue_mailer gem. --- actionmailer/lib/action_mailer/async.rb | 41 ++++++++++++++++++++++ actionmailer/lib/action_mailer/base.rb | 7 ++++ actionmailer/test/base_test.rb | 14 ++++++++ .../test/fixtures/async_mailer/welcome.erb | 1 + actionmailer/test/mailers/async_mailer.rb | 3 ++ 5 files changed, 66 insertions(+) create mode 100644 actionmailer/lib/action_mailer/async.rb create mode 100644 actionmailer/test/fixtures/async_mailer/welcome.erb create mode 100644 actionmailer/test/mailers/async_mailer.rb (limited to 'actionmailer') diff --git a/actionmailer/lib/action_mailer/async.rb b/actionmailer/lib/action_mailer/async.rb new file mode 100644 index 0000000000..6f768ac286 --- /dev/null +++ b/actionmailer/lib/action_mailer/async.rb @@ -0,0 +1,41 @@ +module ActionMailer::Async + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def method_missing(method_name, *args) + if action_methods.include?(method_name.to_s) + QueuedMessage.new(self, method_name, *args) + else + super + end + end + end + + class QueuedMessage + delegate :to_s, :to => :actual_message + + def initialize(mailer_class, method_name, *args) + @mailer_class = mailer_class + @method_name = method_name + *@args = *args + end + + def run + actual_message.deliver + end + + def deliver + Rails.queue << self + end + + def actual_message + @actual_message ||= @mailer_class.send(:new, @method_name, *@args).message + end + + def method_missing(method_name, *args) + actual_message.send(method_name, *args) + end + end +end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 739f9a52a9..f40b85549e 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -456,6 +456,13 @@ module ActionMailer #:nodoc: super || action_methods.include?(method.to_s) end + def async=(truth) + if truth + require 'action_mailer/async' + include ActionMailer::Async + end + end + protected def set_payload_for_mail(payload, mail) #:nodoc: diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 1b2e39b3f7..fdc784029b 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -7,6 +7,9 @@ require 'active_support/time' require 'mailers/base_mailer' require 'mailers/proc_mailer' require 'mailers/asset_mailer' +require 'mailers/async_mailer' + +require 'rails/queueing' class BaseTest < ActiveSupport::TestCase def teardown @@ -419,6 +422,16 @@ class BaseTest < ActiveSupport::TestCase assert_equal(1, BaseMailer.deliveries.length) end + test "delivering message asynchronously" do + Rails.stubs(:queue).returns(Rails::Queueing::TestQueue.new) + AsyncMailer.delivery_method = :test + AsyncMailer.deliveries.clear + AsyncMailer.welcome.deliver + assert_equal(0, AsyncMailer.deliveries.length) + Rails.queue.drain + assert_equal(1, AsyncMailer.deliveries.length) + end + test "calling deliver, ActionMailer should yield back to mail to let it call :do_delivery on itself" do mail = Mail::Message.new mail.expects(:do_delivery).once @@ -434,6 +447,7 @@ class BaseTest < ActiveSupport::TestCase end test "should raise if missing template in implicit render" do + BaseMailer.deliveries.clear assert_raises ActionView::MissingTemplate do BaseMailer.implicit_different_template('missing_template').deliver end diff --git a/actionmailer/test/fixtures/async_mailer/welcome.erb b/actionmailer/test/fixtures/async_mailer/welcome.erb new file mode 100644 index 0000000000..01f3f00c63 --- /dev/null +++ b/actionmailer/test/fixtures/async_mailer/welcome.erb @@ -0,0 +1 @@ +Welcome \ No newline at end of file diff --git a/actionmailer/test/mailers/async_mailer.rb b/actionmailer/test/mailers/async_mailer.rb new file mode 100644 index 0000000000..ce601e7343 --- /dev/null +++ b/actionmailer/test/mailers/async_mailer.rb @@ -0,0 +1,3 @@ +class AsyncMailer < BaseMailer + self.async = true +end -- cgit v1.2.3 From 0cb154be245f15708a126f49b46ac6cd7f08ea73 Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Sat, 23 Jun 2012 14:45:16 -0400 Subject: AsyncMailer documention --- actionmailer/lib/action_mailer/async.rb | 2 ++ actionmailer/lib/action_mailer/base.rb | 1 + 2 files changed, 3 insertions(+) (limited to 'actionmailer') diff --git a/actionmailer/lib/action_mailer/async.rb b/actionmailer/lib/action_mailer/async.rb index 6f768ac286..cbadd3dc25 100644 --- a/actionmailer/lib/action_mailer/async.rb +++ b/actionmailer/lib/action_mailer/async.rb @@ -26,10 +26,12 @@ module ActionMailer::Async actual_message.deliver end + # Will push the message onto the Queue to be processed def deliver Rails.queue << self end + # The original ActionMailer message def actual_message @actual_message ||= @mailer_class.send(:new, @method_name, *@args).message end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index f40b85549e..5686e78dfc 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -456,6 +456,7 @@ module ActionMailer #:nodoc: super || action_methods.include?(method.to_s) end + # Will force ActionMailer to push new messages to the Rails.queue def async=(truth) if truth require 'action_mailer/async' -- cgit v1.2.3 From dc9f6fc046dac3bf26dd37625e977e57a129ba8f Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Sat, 23 Jun 2012 15:10:49 -0400 Subject: Force message delivery despite async --- actionmailer/lib/action_mailer/async.rb | 10 ++++++++-- actionmailer/test/base_test.rb | 8 ++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) (limited to 'actionmailer') diff --git a/actionmailer/lib/action_mailer/async.rb b/actionmailer/lib/action_mailer/async.rb index cbadd3dc25..fd62c57790 100644 --- a/actionmailer/lib/action_mailer/async.rb +++ b/actionmailer/lib/action_mailer/async.rb @@ -27,8 +27,14 @@ module ActionMailer::Async end # Will push the message onto the Queue to be processed - def deliver - Rails.queue << self + # To force message delivery dispite async pass `true` + # Emailer.welcome.deliver(true) + def deliver(force = false) + if force + run + else + Rails.queue << self + end end # The original ActionMailer message diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index fdc784029b..908ce719b5 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -432,6 +432,14 @@ class BaseTest < ActiveSupport::TestCase assert_equal(1, AsyncMailer.deliveries.length) end + test "forcing message delivery despite asynchronous" do + Rails.stubs(:queue).returns(Rails::Queueing::TestQueue.new) + AsyncMailer.delivery_method = :test + AsyncMailer.deliveries.clear + AsyncMailer.welcome.deliver(true) + assert_equal(1, AsyncMailer.deliveries.length) + end + test "calling deliver, ActionMailer should yield back to mail to let it call :do_delivery on itself" do mail = Mail::Message.new mail.expects(:do_delivery).once -- cgit v1.2.3 From d1d6c364d0287a5aae667332ed9a9b25990fd207 Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Sat, 23 Jun 2012 15:21:33 -0400 Subject: Updated Changelogs --- actionmailer/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) (limited to 'actionmailer') diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index a822412090..4d8f739403 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -2,6 +2,8 @@ * Raise an `ActionView::MissingTemplate` exception when no implicit template could be found. *Damien Mathieu* +* Asynchronously send messages via the Rails Queue *Brian Cardarella* + ## Rails 3.2.5 (Jun 1, 2012) ## * No changes. -- cgit v1.2.3 From 5337149caf28b9d8bfe907584511453044890ebd Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Sat, 23 Jun 2012 16:06:04 -0400 Subject: Use Delegator for a cleaner QueuedMessage class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Credit goes to *Nicolás Sanguinetti* (foca) for this suggestion --- actionmailer/lib/action_mailer/async.rb | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) (limited to 'actionmailer') diff --git a/actionmailer/lib/action_mailer/async.rb b/actionmailer/lib/action_mailer/async.rb index fd62c57790..620f47f060 100644 --- a/actionmailer/lib/action_mailer/async.rb +++ b/actionmailer/lib/action_mailer/async.rb @@ -1,3 +1,5 @@ +require 'delegate' + module ActionMailer::Async def self.included(base) base.extend(ClassMethods) @@ -13,17 +15,19 @@ module ActionMailer::Async end end - class QueuedMessage - delegate :to_s, :to => :actual_message - + class QueuedMessage < ::Delegator def initialize(mailer_class, method_name, *args) @mailer_class = mailer_class @method_name = method_name *@args = *args end + def __getobj__ + @actual_message ||= @mailer_class.send(:new, @method_name, *@args).message + end + def run - actual_message.deliver + __getobj__.deliver end # Will push the message onto the Queue to be processed @@ -36,14 +40,5 @@ module ActionMailer::Async Rails.queue << self end end - - # The original ActionMailer message - def actual_message - @actual_message ||= @mailer_class.send(:new, @method_name, *@args).message - end - - def method_missing(method_name, *args) - actual_message.send(method_name, *args) - end end end -- cgit v1.2.3 From dee0b23af22516f52892fbf6af635fc7d1689a0e Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Sat, 23 Jun 2012 16:38:44 -0400 Subject: Removed unecessary splatting --- actionmailer/lib/action_mailer/async.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'actionmailer') diff --git a/actionmailer/lib/action_mailer/async.rb b/actionmailer/lib/action_mailer/async.rb index 620f47f060..6fcc94d50e 100644 --- a/actionmailer/lib/action_mailer/async.rb +++ b/actionmailer/lib/action_mailer/async.rb @@ -19,7 +19,7 @@ module ActionMailer::Async def initialize(mailer_class, method_name, *args) @mailer_class = mailer_class @method_name = method_name - *@args = *args + @args = args end def __getobj__ -- cgit v1.2.3 From 812d1e88c5218d4075878559476eaad65df1b761 Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Sat, 23 Jun 2012 23:42:03 -0400 Subject: Support for custom queues on the mailer Credit goes to *Aaron Patterson* (tenderlove) --- actionmailer/lib/action_mailer/async.rb | 25 +++++++++++++------------ actionmailer/lib/action_mailer/base.rb | 2 +- actionmailer/test/base_test.rb | 21 ++++++++++++++++----- 3 files changed, 30 insertions(+), 18 deletions(-) (limited to 'actionmailer') diff --git a/actionmailer/lib/action_mailer/async.rb b/actionmailer/lib/action_mailer/async.rb index 6fcc94d50e..d7c34c1a7b 100644 --- a/actionmailer/lib/action_mailer/async.rb +++ b/actionmailer/lib/action_mailer/async.rb @@ -1,22 +1,23 @@ require 'delegate' module ActionMailer::Async - def self.included(base) - base.extend(ClassMethods) + def method_missing(method_name, *args) + if action_methods.include?(method_name.to_s) + QueuedMessage.new(queue, self, method_name, *args) + else + super + end end - module ClassMethods - def method_missing(method_name, *args) - if action_methods.include?(method_name.to_s) - QueuedMessage.new(self, method_name, *args) - else - super - end - end + def queue + Rails.queue end class QueuedMessage < ::Delegator - def initialize(mailer_class, method_name, *args) + attr_reader :queue + + def initialize(queue, mailer_class, method_name, *args) + @queue = queue @mailer_class = mailer_class @method_name = method_name @args = args @@ -37,7 +38,7 @@ module ActionMailer::Async if force run else - Rails.queue << self + @queue << self end end end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 5686e78dfc..1f6c00ce11 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -460,7 +460,7 @@ module ActionMailer #:nodoc: def async=(truth) if truth require 'action_mailer/async' - include ActionMailer::Async + extend ActionMailer::Async end end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 908ce719b5..f1988a8230 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -422,22 +422,33 @@ class BaseTest < ActiveSupport::TestCase assert_equal(1, BaseMailer.deliveries.length) end + def stub_queue klass, queue + Class.new(klass) { + extend Module.new { + define_method :queue do + queue + end + } + } + end + test "delivering message asynchronously" do - Rails.stubs(:queue).returns(Rails::Queueing::TestQueue.new) + testing_queue = Rails::Queueing::TestQueue.new AsyncMailer.delivery_method = :test AsyncMailer.deliveries.clear - AsyncMailer.welcome.deliver + stub_queue(AsyncMailer, testing_queue).welcome.deliver assert_equal(0, AsyncMailer.deliveries.length) - Rails.queue.drain + testing_queue.drain assert_equal(1, AsyncMailer.deliveries.length) end test "forcing message delivery despite asynchronous" do - Rails.stubs(:queue).returns(Rails::Queueing::TestQueue.new) + testing_queue = Rails::Queueing::TestQueue.new AsyncMailer.delivery_method = :test AsyncMailer.deliveries.clear - AsyncMailer.welcome.deliver(true) + stub_queue(AsyncMailer, testing_queue).welcome.deliver(true) assert_equal(1, AsyncMailer.deliveries.length) + assert_predicate testing_queue, :empty? end test "calling deliver, ActionMailer should yield back to mail to let it call :do_delivery on itself" do -- cgit v1.2.3 From 33334d0ea8fed091a145ef04b48d103292d6b4e7 Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Sun, 24 Jun 2012 04:21:02 -0400 Subject: Forcing the message sending is no longer necessary --- actionmailer/lib/action_mailer/async.rb | 8 +------- actionmailer/test/base_test.rb | 9 --------- 2 files changed, 1 insertion(+), 16 deletions(-) (limited to 'actionmailer') diff --git a/actionmailer/lib/action_mailer/async.rb b/actionmailer/lib/action_mailer/async.rb index d7c34c1a7b..049d6afaea 100644 --- a/actionmailer/lib/action_mailer/async.rb +++ b/actionmailer/lib/action_mailer/async.rb @@ -32,14 +32,8 @@ module ActionMailer::Async end # Will push the message onto the Queue to be processed - # To force message delivery dispite async pass `true` - # Emailer.welcome.deliver(true) def deliver(force = false) - if force - run - else - @queue << self - end + @queue << self end end end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index f1988a8230..84e5399924 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -442,15 +442,6 @@ class BaseTest < ActiveSupport::TestCase assert_equal(1, AsyncMailer.deliveries.length) end - test "forcing message delivery despite asynchronous" do - testing_queue = Rails::Queueing::TestQueue.new - AsyncMailer.delivery_method = :test - AsyncMailer.deliveries.clear - stub_queue(AsyncMailer, testing_queue).welcome.deliver(true) - assert_equal(1, AsyncMailer.deliveries.length) - assert_predicate testing_queue, :empty? - end - test "calling deliver, ActionMailer should yield back to mail to let it call :do_delivery on itself" do mail = Mail::Message.new mail.expects(:do_delivery).once -- cgit v1.2.3 From aee4eec47aca73cdc5515b70c041654a3d2e8b4c Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Sun, 24 Jun 2012 04:28:52 -0400 Subject: Better documentation for ActionMailer.async --- actionmailer/lib/action_mailer/base.rb | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'actionmailer') diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 1f6c00ce11..272e68a3b5 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -457,6 +457,10 @@ module ActionMailer #:nodoc: end # Will force ActionMailer to push new messages to the Rails.queue + # when set to true + # + # class WelcomeMailer < ActionMailer::Base + # self.async = true def async=(truth) if truth require 'action_mailer/async' -- cgit v1.2.3 From a11fcd9a7681b2c064f15cc7993946e82261a8b3 Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Sun, 24 Jun 2012 04:30:29 -0400 Subject: Better documentation for ActionMailer.async --- actionmailer/lib/action_mailer/base.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'actionmailer') diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 272e68a3b5..89e0adc04d 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -456,8 +456,8 @@ module ActionMailer #:nodoc: super || action_methods.include?(method.to_s) end - # Will force ActionMailer to push new messages to the Rails.queue - # when set to true + # Will force ActionMailer to push new messages to the queue defined + # in the ActionMailer class when set to true # # class WelcomeMailer < ActionMailer::Base # self.async = true -- cgit v1.2.3 From 35717a9370e4d0ee312aa591bc2674004cfbadba Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Sun, 24 Jun 2012 17:17:06 -0400 Subject: Some final syntax fixes --- actionmailer/lib/action_mailer/async.rb | 2 +- actionmailer/test/base_test.rb | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'actionmailer') diff --git a/actionmailer/lib/action_mailer/async.rb b/actionmailer/lib/action_mailer/async.rb index 049d6afaea..86a5b0d035 100644 --- a/actionmailer/lib/action_mailer/async.rb +++ b/actionmailer/lib/action_mailer/async.rb @@ -32,7 +32,7 @@ module ActionMailer::Async end # Will push the message onto the Queue to be processed - def deliver(force = false) + def deliver @queue << self end end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 84e5399924..144a6bfe39 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -8,7 +8,6 @@ require 'mailers/base_mailer' require 'mailers/proc_mailer' require 'mailers/asset_mailer' require 'mailers/async_mailer' - require 'rails/queueing' class BaseTest < ActiveSupport::TestCase @@ -422,7 +421,7 @@ class BaseTest < ActiveSupport::TestCase assert_equal(1, BaseMailer.deliveries.length) end - def stub_queue klass, queue + def stub_queue(klass, queue) Class.new(klass) { extend Module.new { define_method :queue do -- cgit v1.2.3