29

The common pattern for interfacing with ActiveJob in Rails is to set up a Job with a perform() method that gets called asynchronously via perform_now or perform_later

In the special case of Mailers, you can directly call deliver_now or deliver_later since ActiveJob is well integrated with ActionMailer.

The rails documentation has the following comments -

# If you want to send the email now use #deliver_now
UserMailer.welcome(@user).deliver_now
 
# If you want to send the email through Active Job use #deliver_later
UserMailer.welcome(@user).deliver_later

The wording makes it seem like deliver_now will not use ActiveJob to send the mail. Is that correct, and if so what's the true difference between deliver_now and deliver_later? Is one not asynchronous?

Similarly, does the same difference apply to perform_now and perform_later ?

Thanks!

Jarl
  • 2,831
  • 4
  • 24
  • 31
user2490003
  • 10,706
  • 17
  • 79
  • 155
  • To me it seems bad that ActionMailer and ActionJob mess with eachother like this. ActionJob should be agnostic to what the job is doing. ActionMailer should be able to be delayed by ActionJob without having to know that it is being delayed, just like any other object. To me this seems broken in Rails 3, 4, and 5. – nroose Apr 24 '18 at 00:15

2 Answers2

38

As you say in your question, deliver_now does not use ActiveJob.

Basically, deliver_later is asynchronous. When you use this method, the email is not send at the moment, but rather is pushed in a job's queue. If the job is not running, the email will not be sent. deliver_now will send the email at the moment, no matter what is the job's state. Here you can see the documentation for deliver methods.

According to your second question, perform_now will process the job immediately without sending to the queue. perform_later, however, will add the job to the queue, and as soon the job's queue is free, will perform the job. Here you can see the documentation for perform methods.

Daniel Batalla
  • 1,184
  • 1
  • 11
  • 22
  • what if the browser connection is closed? Still deliver_later will be executed? – gates Apr 07 '16 at 09:29
  • 3
    @gates `deliver_later` is managed on server side, so it does not matter if the browser connection is closed. Once a job is pushed to the queue, it will be sent (unless you kill the job process). – Daniel Batalla Apr 08 '16 at 13:54
  • What is the behavior change if called from a script, executed via 'rails runner bin/send-lots-of-email.rb' I wonder? – Michael Graff Jul 11 '17 at 18:10
7

In addition to what Daniel Batalla wrote, here's one more observation that I made: deliver_later seems to perform lazy evaluation, while deliver_now does not.

I have an ActiveRecord model with an additional attribute reset_token that is not stored in the database (this is from Michael Hartl's railstutorial.org; the model stores a hashed version of the token in the reset_digest column).

When executing deliver_now, accessing the @model's reset_token attribute inside the mailer view yields the reset token as expected. However, when executing deliver_later, @model.reset_token is always nil. It appears as if deliver_later updates the model with database data, and because reset_token is an additional attribute that is not backed by the database, it will be nil at that point. (The code documentation is too deeply nested for me to be able to verify this in the source.)

Michael uses deliver_now in the tutorial. I don't know he does this to avoid lazy evaluation. But it took me a while to realize that I just had to change deliver_later to deliver_now to make my tests pass.

bovender
  • 1,838
  • 15
  • 31
  • As the `deliver_later` is asynchronous, it would require to reload the object. Like in any case of any asynchronous job. As it is required to take the new changes, if any, made to the job. – Monika Mar 10 '16 at 08:48
  • 3
    Thank you, @bovender. I just had the same issue. In my Model there are a few methods to calculate total/gross prices and if I run mailer as follows `Mailer.payment_email(model, calculate_price).deliver_later`, `price` is always `nil`. Usage of `deliver_now` helps, but it ruins all benefits of async delivery. To overcome it I replaced `calculate_price` with `calculate_price.to_f`, it forces a method evaluation. In other cases it may be `to_i`, `to_s`, etc, depending on the data type returning by the method. – ololobus Jan 31 '17 at 22:44
  • @ololobus How would one do deliver_later while keeping the current object state? In my case, I'm looking to access Active Record's dirty methods. Ex: record.changes. Deliver now works but deliver later has no changes. – karns Oct 28 '22 at 16:24