Thursday, March 26, 2009

Send mails asynchronous from a Rails app



Dealing with mails is not an easy task, sending mails synchronously is not an option, because maybe a user wants to send invitations to lots of people and this takes time, or even if it is only one, maybe there is another user trying to send another invitation and the smtp will collapse.




I looked for a plugin or something that could handle this but I didn't like most of them.
The option that I liked most was "ar_mailer", the plugin basically saves the mails to the database and provide you with a process for send them later, programming this process with a cron job or running it as a daemon. But it does not manage prioritys!, so for example in a common web app suppose a user wants to send invitations and another user wants to reset his password. If the user sending invitations starts first, the user requesting his password will have to wait for all the invitations to be sent before he could receive the password reset. If you decide to send the urgent mails directly without going thru ar_mailer is the same, because maybe your smtp is busy sending invitations emails and it will refuse to send the urgent ones. So you must have prioritys!, there are some solutions there but they are a little bit complicated.




So I come up with a very easy solution that do what ar_mailer do but in a simpler way, and that could handle prioritys, I am not saying it is better than ar_mailer because is a LOT more "amateurish" but for this problem I could not use ar_mailer.




What I did was create a model "MailQueue", this is the migration for the model:






class CreateMailQueue < ActiveRecord::Migration
def self.up
create_table :mail_queue do |t|
t.text :mail
t.integer :priority, :default => 0
t.timestamps
end
end

def self.down
drop_table :mail_queue
end
end






and this is the MailQueue class:







class MailQueue < ActiveRecord::Base
set_table_name 'mail_queue'

serialize :mail

#/* the mail object generated with the 'create' method of the mailer
# * the priority of the email, the highest, the most
# */
def self.enqueue(mail, priority = 0)
MailQueue.create(:mail => mail, :priority => priority)
end

#/* max: max amount of mails to send per method call
# * time: the maximum amount of time the method should run(in minutes)
# * chunks: the amount of mail to send without checking
# * if there are new ones with higher priority
# */
def self.deliver(max, time, chunks = 5)
init_time = Time.now
delivered = 0
while true
queued_mails = MailQueue.find(:all, :order => "priority DESC, created_at", :limit => chunks)
return if queued_mails.empty?
queued_mails.each do |queued_mail|
return if (delivered == max) || (Time.now - init_time > time * 60)
begin
ActionMailer::Base.deliver(queued_mail.mail)
queued_mail.delete
delivered += 1
rescue
# wait for the smtp to react
sleep 10
end
end
end
end

end





notice I change the model default table name because "mail_queueS" was not very semantic...





The model class has 2 class methods:




The first one creates a MailQueue with a mail attribute, which is the saved mail generated by ActionMailer serialized to the database, and sets a priority for this mail. (Remember that with an ActionMailer class, is possible to use: "deliver_method_name" to send it instantly or "create_method_name" to save it for send it later.)
Serializing is done thru ActiveRecord's serialize method which is called at the beginning of the class. You can call this method in your controller.




The second is supposed to be call with script/runner and automate it with cron to run in a periodical basics. The way in which you decide to run it depends totally on the server environmet you have, for example if you use a shared hosting, most of them limits the amounts of email that can be send per hour. So I add some parameters to customize this behaviour, they are explained at the beginning of the method. Basically this method chechs the MailQueue and retrieves them in "chunks" to send them one by one. Each time it querys the database it orders the queue first by priority and then by the date it was created.




So in your controller, instead of doing:






Notifier.deliver_invite(...)





being Notifier the ActionMailer class, you will do something like this:






MailQueue.enqueue(Notifier.create_invite(...), 0)





and sets a priority in the last argument, the default is 0, so in this case you don't need to put it, but the higher the priority number is, first is going to be retrieved in the queue.
So for the non urgent mails I set a priority of 0, and for the urgent ones I set a higher priority, basically I have 0 and 1, but maybe you need extremely urgent mails, you can set a priority of 2 or higher. After the first "chunk"(I don't know if it is the appropriate word, but I like it in this context) of mails is sent, it queries again to see if another mail was created with higer priority. It's possible to set the chunk, but the default is 5.




So you have to add a cron task for example to run this every hour.
script/runner can be called this way:





>path_to_rails_app/script/runner 'MailQueue.deliver(200, 55)'




to deliver a max of 200 emails in a maximum period of 55 minutes.




Another detail is that changing this method a little bit, it could be set to run as a daemon(I mean never ending..), but first is an instance of rails that you must have up all the time so it's not very efficient, and second maybe it hungs up and you didn't notice, so I think the safest and efficient way is running it with a cron task, and tell the method to run for a maximum specific period of time.




I think the code speaks for itself, if you have any questions or remarks, please comment or send an email.




NOTE: Now there are other options like delayed_job which is great, anyway I think the code is interesting.