Do delayed Sidekiq Mailer methods execute when the job is processed? - ruby-on-rails

I have a typical ActionMailer with a method specifying an email delivery.
def some_email(user_id)
#user = User.find(user_id)
if #user.eligible_for_email?
mail(to: #user.email, from: "me#me.com", subject: "The Subject")
#user.email_sent = Date.today
#user.save
end
end
I want to delay the sending of this using Sidekiq so I use:
Mailer.delay_for(2.days).some_email(user.id)
The eligible_for_email method:
def eligible_for_email?
!unsubscribed? && email_sent.nil?
end
In the meantime, the user could have unsubscribed, which is why there is a method in the User model called eligible_for_email? which I can use to conditionally send the email - but obviously this condition needs to be tested just before the email is sent, not when the job is scheduled.
So the problem is that when I use Sidekiq to process this, the conditional logic doesn't seem to be run when the job is done.
Does Sidekiq work by executing the some_email method on runtime and then queuing the resulting email to be sent out two days later, thereby negating my conditional code?

Your understanding is 100% correct and that's exactly what you want to do.
I'd guess you aren't restarting Sidekiq to pick up your code changes. Sidekiq does not auto-reload changed code like Rails does.

Related

Is it possible to forward the mail object in Action Mailer

We're currently letting users email each other without seeing each other's actual email addresses (double blind) by letting them send emails to username#parse.example.com which works great.
class ForwardsMailbox < ApplicationMailbox
before_processing :ensure_users
def process
content = mail.multipart? ? mail.parts.first.body.decoded : mail.decoded
UserMailer.with(sender: sender, recipient: recipient, subject: mail.subject, content: content).forward_email.deliver_later
end
private
def sender
#sender ||= User.find_by(email: mail.from.first)
end
def recipient
#recipient ||= User.find_by(username: mail.to.first.split('#').first)
end
def ensure_users
bounce_with UserMailer.invalid_user(inbound_email) if sender.nil? or recipient.nil?
end
end
Is it possible to forward the whole mail object instead of extracting its contents, checking if it is multipart etc?
Try to give this a shot. Reuse the mail object in your process method and directly deliver the message yourself. You'll need to reach into ActionMailer to get your delivery methods configured correctly, but I believe it'll work.
def process
mail.to = sender.email
mail.from = recipient...
ActionMailer::Base.wrap_delivery_behavior(mail) # this sets delivery to use what we specified in our rails config.
mail.deliver # This delivers our email to the smtp server / API
end
How this works:
Behind the scenes Mailers are just calling deliver on a Mail object to send emails. You can go peruse through ActionMailer::MessageDelivery if you'd like to see that in action. We're just using that functionality directly here.
I'd recommend staying away from using Mailers in this instance because copying over all the fields from your original mail object to the mailer's mail object is going to require a lot of trial and error.
One thing to note: the headers remain unchanged when the message is re-delivered, so things like Message-ID will still be the same (which may or may not be a problem, just something to consider).
Last, if you're concerned at all about deliver being a blocking call to an API/SMTP server like I was, worry not! It looks like ActionMailbox already ensures that the process method runs via ActiveJob, so you shouldn't have to worry about an SMTP/API request taking a while and blocking a web request (see ActionMailbox guides).

delayed_job saving entry but not sending out email

I am using delayed_job gem to send email upon the destroy of a record.
comments_controller.rb
def reject_appeal
#comment = Comment.find(params[:id])
if #comment.destroy
#comment.rejection(#comment)
flash[:alert] = "You have succesfully rejected the appeal."
redirect_to post(#comment.post_id)
end
end
comments.rb
def rejection(comment)
RejectAppeal.delay.notify(comment)
end
Now if I remove the delay method and just have it as RejectAppeal.notify(comment), the email gets sent out perfectly fine. But with delay i don't know what happens. I don't see anything in the delayed job lob log. Although in development log i do notice that the entry gets stored in or at least it BEGINS the action.
Any help on this? I am using delay method on several other mailer functions in this same app and they all get sent out fine but i am unsure whats wrong with this
P.S I am using Rails 4.0.1
Is the delayed job getting serialized correctly? (Check the delayed_jobs table). Is there an error running the job? The error gets stored in the last_error column if so.
It's probably because you are destroying your comment and then passing it through to the delayed job so, when the delayed job runs and tries to load the comment to do something with it, the comment can no longer be found.

Execute task in another thread

I'm new in Rails developing, and I have one question. There the following code:
def create
#order = current_user.orders.create!(order_params)
OrderMailer.send_order_info(#order).deliver
end
This code creates a new order, render json result and send e-mail about it. Mail sending takes some time, and I think I should do it in another thread or something similar. Please, give me advice how I can do it good. Thanks!
You should use delay the email sending. You could do it using Sidekiq, Delayed Job or Resque for example.
You will also be able to delay any other jobs with these gems.
You should look at the docs and see which one is the best for your use.
I personally use Sidekiq but Delayed Job is the easiest to install if you only want to use it for mailer.
We use Spawn for this: it's changed its name to "Spawnling" now
https://github.com/tra/spawnling
Very easy to use: (in the controller)
#user = User.create(params[:user])
spawn do
#user.do_some_slow_background_stuff
end
or, if you want to monitor whether the background process has finished yet (#spawn_id is the pid)
#user = User.create(params[:user])
spawner = spawn do
#user.do_some_slow_background_stuff
end
#spawn_id = spawner.handle

Sending mass emails without background job

I want to automatically send out emails to a list of users from my Rails app.
The volume ranges from really a few users (5-10) to groups of users (50-70).
The maximum would be all users (currently 5000).
I have understood that sending emails can block the Rails app, so sending emails should be done with the help of a queueing system and a background job, e.g. by using DelayedJob, Resque or Sidekiq.
Unfortunately, having such a background job requires a worker process on the Heroku platform. And I want to avoid that due to the increased cost (at least for the beginning).
Is there any alternative approach that I could take? E.g., a second Rails app with just one worker process which only does the email handling (well I guess that would then also result in a paid worker process)?
Is it possible to send out mass emails without such a worker process via SendGrid, MailGun or any other service that integrates nicely with Heroku?
Props to #phoet for his link
We've got a similar idea working on Heroku which we thought would be free. Turns out they billed us for the scheduler hours... but here's what we did:
Resque
Resque is a queueing system which runs on Rails to queue items in Redis. It's totally recommended to use this on Heroku, and is very efficient & scalable
It works like this:
Send "data" (typically ID's) to resque queue
Resque sends ID's to Redis
The Resque rake job processes the Redis Queue
Perform your mailout when queue is processed (sending the email to Mandrill / SendGrid)
The reason for having a queue is as #apneadiving said - your controller will timeout AND (more importantly), your Rails app will lock up until the process has completed
There is a very good Railscast on Resque here:
Code
This is just basic code - can add more if you want:
#app/controllers/messages_controller.rb
def send_message
id = params[:id]
message = Message.find(id).broadcast!
flash[:notice] = "Broadcast Queued!"
redirect_to root_path
end
#app/models/message.rb
def broadcast!
self.subscribers.each do |subscriber|
Resque.enqueue(MailoutQueue, id, subscriber.id, queue.id)
end
end
#app/workers/mailout_queue.rb
class MailoutQueue
#queue = :mailer
def self.perform(message_id, recipient_id, queue_id)
MessageMailer.send_message(message_id, recipient_id).deliver
end
end
class MessageMailer < ActionMailer::Base
default from: '****************'
def send_message(message_id, subscriber_id)
#Declarations
#message = Message.find(message_id)
#subscriber = Subscriber.find(subscriber_id)
#Send
mail(:to => #subscriber.email, :subject => #message.title)
end
end
Take a look here - https://github.com/stephenb/sendgrid#delivering-to-multiple-recipients
This allows you to send multiple emails with only one SMTP call.

How can I abort the delivery of an ActionMailer request?

I'm running a Q&A service. One of the things admins can do is mark a question as offtopic. When they they do that an email gets sent to the person that asked the question telling them that their email is offtopic.
The email notification is sent via delayed_job:
QuestionMailer.delay.notify_asker_of_offtopic_flag question
However, on occasion someone might accidentally mark the question as offtopic or change their mind. To avoid an incorrect notification going to the person who originally asked it I want to create a short delay and evaluate whether the question is still offtopic when the mailer request runs:
Delayed call to mailer:
QuestionMailer.delay(run_at: Time.now + 3.minutes).notify_asker_of_offtopic_flag(question)
Mailer:
class QuestionMailer
...
def notify_asker_of_offtopic_flag question
if question.offtopic?
# do mailing
end
end
end
Unfortunately this isn't that simple since the if block simply causes an error which then causes delayed_job to retry the job again and again.
I'm now experimenting with some pretty roundabout methods to achieve the same end but I'd really like to find some way to abort the QuestionMailer action without triggering errors. Is this possible?
Dont delay the mailer then. Delay another class method in your Question class perhaps? Pass the id of the question and within that delayed method check if the question is still offtopic and if yes the send email synchronously.
Essentially, your notify_asker_of_offtopic_flag could be moved to your question model and then mailing is synchronous (i'm sure you'll rename your methods).
There is talk going on about preventing delivering by setting perform_deliveries to false within your mail action itself in core but i'm not 100% where or how that will end up.
#Aditya's answer was basically correct however I wanted to keep my methods on the Mailer object to keep things nice and tidy. This required a few extra hacks.
Create a new Class method in the mailer that CAN be delayed
The problem with trying to cancel an instance Mailer method is that it inherently triggers rendering and other things that stop the method from being aborted. However I would still like to keep all my Mailer logic together.
The way I did this was by using a class method instead of an instance method. This avoided all of the hooks that kick in when calling the method on an ActionMailer instance but still allowed me to keep the code tidy and together
class QuestionMailer
...
def notify_asker_of_offtopic_flag question
...
end
def self.notify_asker_of_offtopic_flag question_if question
if question.offtopic?
QuestionMailer.notify_asker_of_offtopic_flag question
end
end
end
NB fix for using delayed job
This works except for one slight hack that's necessary to deal with delayed_job.
When dealing with a Mailer, delayed_job will always call .deliver on the returned object in order to deliver the mail. This is fine when we return a mail object but in this case we're returning nil. delayed_job therefore tries to call .deliver on nil and everything fails.
In order to account for this we simply return a dummy mailer object containing a dupe .deliver method:
class QuestionMailer
...
class DummyMailer
def deliver
return true
end
end
def notify_asker_of_offtopic_flag question
# do mailing stuff
end
def self.notify_asker_of_offtopic_flag question_if question
if question.offtopic?
QuestionMailer.notify_asker_of_offtopic_flag question
else
DummyMailer.new
end
end
end

Resources