DelayedJob email where to have 'who to email' logic - ruby-on-rails

In my app I have certain events that trigger a lot of emails (~100). Obviously sending them immediately is not an option, so I'm using DelayedJob to queue them up and send them after the request is processed. I've now found that the logic to determine WHICH 100 people to email is heavy enough that it takes a while to run, so I'd like to DelayedJob that process as well. Where should this logic go? (model? mailer?) Sending mail from the model just feels wrong. Is there a best practice here?

You should write a class that represents the job. Not a model class, not a controller class: a job class.
# app/jobs/mail_job.rb
class MailJob
attr_accessor :first_option, :second_option
def initialize(first_option, second_option)
self.first_option = first_option
self.second_option = second_option
end
def perform
accounts = Account.where("some_key" => first_option).to_a
# more complicated stuff goes here
accounts.each do |account|
AccountMailer.hello_message(account).deliver
account.mark_hello_delivered!
end
end
end
job = MailJob.new(params["first"], params["second"])
Delayed::Job.enqueue(job)

Related

Best methods for combining Active Job and Action Mailer?

When using Rails Action Mailer, you have the option to use deliver_now to send the email immediately, or deliver_later to send through asynchronously using Active Job. If Active Job is not specified an adapter, it will use an in-process thread pool which would not persist if the server were to stop. Alternatively, I can create a Job to manage my e-mails, which I can then call with perform_now or perform_later, which as I understand is more or less the exact same thing as deliver_now and deliver_later.
My question is, if I specify an adapter, let's say Sidekiq, and then have a database to store my jobs, why would I create a job to handle my e-mails? Is there any additional benefit, or is it an unnecessary step? On a slightly different note, if I did want to create a job for the process, would my email method need to have deliver_now or simply nothing at all? I presume if the e-mail were to say deliver_later, would it knock back the email to the end of the queue and force it wait again until it is sent?
To illustrate, if I have no Job set up to handle my emails, I could simply have:
class UserMailer < ActionMailer::Base
def send_email(user)
mail(to: user.email, subject: "My Subject")
end
end
To call, I would use:
UserMailer.send_email(my_user).deliver_later.
However, if I had wanted to add a job, and both options were called with UserJob.perform_later(user), would my setup be:
class UserJob < ActiveJob::Base
def perform(user)
UserMailer.send_email(user).deliver_now
end
end
Or
def perform(user)
UserMailer.send_email(user)
end
Lastly, I don't think this makes sense, but what would happen if I used:
def perform(user)
UserMailer.send_email(user).deliver_later
end

Design Decision using ActionMailer

I'm confused about what design decision I need to make.
I know the fundamental use case of ActionMailer in that you would:
Create a class inheriting from ActionMaier
define a method in there of what to send and who to send it to and from who, etc
This is where I deviate and lose understanding.
Typically, you would call the method on your mailer model in the controller of a certain action, for ex-
def create
#user = User.new(params[:user])
if #user.save
MyMailerClass.send_signup_email(#user).deliver
redirect_to #user
else
render :new
end
end
What if for example I want to send an email based on if the user hasn't signed in in X days. I'm sure there are a number of ways to do this but curious of the rails best practice way, as I'm not sure where to look. This is close I don't want it to send depending on the time of day just when the certain condition is met.
Thanks.
It sounds like you want a cron job that runs periodically to check certain conditions and based on those conditions, send an email. This isn't really too far off from what you do with a standard mailer, except rather than sending it from the controller, you would be sending it from a separate script. I usually write a rake task for this:
task :send_reminder_emails => :environment do
users = User.where('last_login <= ?', 10.days.ago)
users.each do |user|
MyMailerClass.reminder(user).deliver
end
end
And you would run it with rake send_reminder_emails. There are tons of examples available for how to setup a cron job if you aren't familiar with how to do that.

Change status of an object after a set of async jobs is complete using sidekiq

I'm using sidekiq to deal with async jobs, and after some complexity added, I'm having difficulties to be aware of the state of the jobs.
Here's the deal:
I have a model Batch that calls an async method after it's commited:
# app/models/batch.rb
class Batch < ActiveRecord::Base
after_commit :calculate, on: :create
def calculate
job_id = BatchWorker.perform_async(self.id)
# update_column skips callbacks and validations!
self.update_column(:job_id, job_id)
end
end
The worker reads data from the model and calls an async job for each data, as follows:
# app/workers/batch_worker.rb
class BatchWorker
def perform(batch_id)
batch = Batch.find(batch_id)
## read data to 'tab'
tab.each do |ts|
obj = batch.item.create(name: ts[0], data: ts[1])
job_id = ItemWorker.perform_async(obj.id)
obj.update_attribute(:job_id, job_id)
end
end
end
The problem is: Those async jobs perform calculations, and I can't allow the download results link be available before it's complete, so I need to know when all "children-jobs" are done, so I can change a status attribute from the Batch model. In other words, I don't need to know if all jobs have been queued, but instead, if all async jobs generated by ItemWorker have been performed, and are now complete.
What would be the best way to attain this? Does it make sense in the "parallel computation world"?
Obs.: I'm not sure about storing the job_id in db, since it seems to be volatile.
Perhaps using Redis for this could be a good fit, seeing as you already have it in your infrastructure and configured in your Rails app (due to Sidekiq)
Redis has an inbuilt publish/subscribe engine, as well as atomic operations on keys - making it suitable for managing the type of concurrency you are looking for.
Maybe something roughly like this:
class BatchWorker
def perform(batch_id)
batch = Batch.find(batch_id)
redis = Redis.new
redis.set "jobs_remaining_#{batch_id}", tab.count
redis.subscribe("batch_task_complete.#{batch_id}") do |on|
on.message do |event, data|
if redis.decr("jobs_remaining_#{batch_id}") < 1
#UPDATE STATUS HERE
redis.del "jobs_remaining_#{batch_id}"
end
end
end
tab.each do |ts|
obj = batch.item.create(name: ts[0], data: ts[1])
job_id = ItemWorker.perform_async(obj.id, batch_id)
end
end
end
class ItemWorker
def perform item_id, batch_id=nil
#DO STUFF
if batch_id
Redis.new.publish "batch_task_complete.#{batch_id}"
end
end
end

How can I call a controller action from ActiveAdmin?

I have this method in my reports_controller.rb, which allows an user to send a status.
def send_status
date = Date.today
reports = current_user.reports.for_date(date)
ReportMailer.status_email(current_user, reports, date).deliver
head :ok
rescue => e
head :bad_request
end
How can I call this action from ActiveAdmin, in order to check if a User sent this report or not? I want it like a status_tag on a column or something.
Should I do a member action?
Thanks!
I'll address the issue of checking if a report has been sent later, but first I'll cover the question of how to call the controller action from ActiveAdmin.
While you can call ReportsController#send_status by creating an ActionController::Base::ReportsController and then calling the desired method, e.g.
ActionController::Base::ReportsController.new.send_status
this isn't a good idea. You probably should refactor this to address a couple potential issues.
app/controllers/reports_controller.rb:
class ReportsController < ApplicationController
... # rest of controller methods
def send_status
if current_user # or whatever your conditional is
ReportMailer.status_email(current_user).deliver
response = :ok
else
response = :bad_request
end
head response
end
end
app/models/user.rb:
class User < ActiveRecord::Base
... # rest of user model
def reports_for_date(date)
reports.for_date(date)
end
end
app/mailers/reports_mailer.rb
class ReportsMailer < ActionMailer::Base
... # rest of mailer
def status_email(user)
#user = user
#date = Date.today
#reports = #user.reports_for_date(#date)
... # rest of method
end
end
This could obviously be refactored further, but provides a decent starting point.
An important thing to consider is that this controller action is not sending the email asynchronously, so in the interest of concurrency and user experience, you should strongly consider using a queuing system. DelayedJob would be an easy implementation with the example I've provided (look into the DelayedJob RailsCast).
As far as checking if the report has been sent, you could implement an ActionMailer Observer and register that observer:
This requires that the User model have a BOOLEAN column status_sent and that users have unique email address.
lib/status_sent_mail_observer.rb:
class StatusSentMailObserver
self.delivered_email(message)
user = User.find_by_email(message.to)
user.update_attribute(:status_sent, true)
end
end
config/intializer/setup_mail.rb:
... # rest of initializer
Mail.register_observer(StatusSentMailObserver)
If you are using DelayedJob (or almost any other queuing system) you could implement a callback method to be called on job completion (i.e. sending the status email) that updates a column on the user.
If you want to track the status message for every day, you should consider creating a Status model that belongs to the User. The status model could be created every time the user sends the email, allowing you to check if the email has been sent simply by checking if a status record exists. This strategy is one I would seriously consider adopting over just a simple status_sent column.
tl;dr ActionController::Base::ReportsController.new.send_status & implement an observer that updates a column on the user that tracks the status. But you really don't want to do that. Look into refactoring like I've mentioned above.

How do you return out of an ActionMailer::Base function?

I'm trying to implement an ActionMailer function that will send out a newsletter to a specific user. I want to make sure that the newsletter is only sent to subscribed users. I tried implementing it like so:
class UserMailer < ActionMailer::Base
def newsletter(user)
return unless user.subscribed # This still renders my mailer view
mail(:to => user.email, :subject => "Newsletter")
end
end
The problem is that the return unless user.subscribed line still appears to be rendering the mailer view and is still sent by the calling code (from a cron job):
task :cron => :environment do
User.where(:subscribed => true).each do |user|
UserMailer.newsletter(user).deliver
end
end
Note that I do have that subscription logic in my cron job as well for performance reasons (shouldn't have to iterate over ALL users, only those that are subscribed). However, it feels like the UserMailer class is the right place for this logic to exist (otherwise any other location that calls the newsletter method will need to check the subscribed flag as well.
The Mailer, IMHO, is the wrong place for this logic. The mailer should do nothing but format and send messages. The logic to decide whether or not to send should be within the calling block of code. It's not the right way, but something as simple as:
UserMailer.newsletter(user).deliver if user.subscribed?
Alternately, as you mentioned, you shouldn't have to iterate over all users, just the subscribed. So with a scope in the User model called subscribed:
User.subscribed.each do |user|
UserMailer.newsletter(user).deliver
end
This way you don't need to test on a per-user basis; only the subscribed users are included, and the logic is in the calling block, not in the mailer.

Resources