Building a Rails Notification Queue 2: The Notification Class
As the title suggests, this is the second post in a series on building a flexible notification queue in Rails. I'd suggest starting with the first post: on the database table (it's quite short).
Our apps create notifications in many places. These notifications are usually tied to lots of different objects or interactions. Unmanaged, this intense coupling becomes a deep dark hole where code quality goes to die.
This post is meant to set up some guiding abstractions so you can keep notifications from becoming a plague on your codebase. A theme you will find is that, by design, this solution is flexible. I urge you to think critically about my decisions as you read through it. While the details below have worked wonderfully for me, your implementation will benefit from some customization.
The class
Enough chat, here is the heart of the app/models/notification.rb
file.
class Notification < ActiveRecord::Base
# Notification Reasons
ACCOUNT_CREATED = 'account_created'.freeze
NEW_MESSAGE = 'new_message'.freeze
belongs_to :user
scope :unsent, -> {
where( cancelled_fl: false, sent_at: nil )
}
def self.schedule( method, to_user_id, reason, scheduled_for, from_user_id=nil, additional_attributes=nil )
Notification.create!(
method: method,
reason: reason,
user_id: to_user_id,
from_user_id: from_user_id,
scheduled_for: scheduled_for,
additional_attributes: additional_attributes
)
end
# Cancels any notifications of a specified reason
# - Use when a user turns off a notification preference
def self.cancel( user_id, reason )
unsent
.where( user_id: user_id, reason: reason )
.update_all( cancelled_fl: true )
end
# Cancels all incoming or outgoing emails from a user
# - Best used when a user deletes
def self.cancel_all_for( user_id )
unsent
.where( 'user_id = ? OR from_user_id = ?', user_id, user_id )
.update_all( cancelled_fl: true )
end
# Cancel a notification instance. ymmv
def cancel!()
self.update_attributes( cancelled_fl: true )
end
def send()
case reason.to_s
when ACCOUNT_CREATED
MailerService.unconfirmed_email( self )
when NEW_MESSAGE
if method == 'push'
PushService.send_new_message( self )
elsif method == 'email'
MailerService.send_new_message( self )
end
else
raise "Unsupported Notification reason: #{reason.inspect}"
end
self.update_attributes( sent_at: Time.now )
end
end
Implementation notes
Skipping the helper methods for cancellations, you will find the real meat of this class is in 2 methods. Notification.schedule()
and notification.send()
do all the heavy listing, so let's explore them.
Notification.schedule()
should handle all inserts into the notification table. Not only is the call shorter than typing Notification.create( {big ass hash} )
, it will give you a single function call that every schedule action goes through. This will pay dividends in the future. The rest of the function is pretty self explanatory.
Also notable is that, in my systems, the from_user_id
is nullable and thus optional while scheduling notifications. Your system may not have a nullable sender, or may not have a sender at all. Adjust this function accordingly.
notification.send()
handles the shepherding of all notifications to the appropriate notification service. This does not actually send emails or pushes. Remember that this is a queue we are trying to process. This class is not responsible for managing vendor api implementations.
Actually scheduling notifications
So now that we have a class to process these notifications, how about a couple of examples of scheduling notifications.
# Send the account created email after a bit
Notification.schedule( :email, user.id, Notification::ACCOUNT_CREATED, Time.now + 30.minutes )
# Send a message email with included message relation
Notification.schedule( :push, user.id, Notification::NEW_MESSAGE, Time.now,
sender.id, {message_id: message.id} )
The first example shows us the power of being able to very easily schedule a notification for arbitrary times in the future. The second example we start to see the power of the additional_attributes
column. Moving forward, new notifications that attach to other tables or objects will not need any database migrations.
Also, does your app tend to use emails and pushes in unison? Make Notification.schedule()
accept an array as the method
. I think you can start to see the flexibility this abstraction provides.
Is that it?
Mostly. The beauty of this class is it's flexibility and how cleanly it organizes the entire notification landscape. In some projects i have found it so useful that i have leveraged it for in app notification and alerts instead of simply outgoing messages.
In the final and all but certainly shorter, post, we will look at building a simple task we can schedule to process this queue in the background, or on another server entirely. You many have been wondering why we haven't called send()
yet. We will.
td;dr: Using a notification class we can abstract all of the scheduling and cancelling logic to a central place.