Closed srb- closed 5 months ago
Hey folks! So here is an example of a notification system and how it should be structured for this project.
# notifications
gem "noticed", "~> 1.6.0"
app
> notifications
> new_aircraft_created_notification.rb
> mailers
> aircraft_mailer.rb
> views
> aircraft_mailer
> created_email.html.erb
NewAircraftCreatedNotification.with(aircraft: aircraft).deliver_later(NewAircraftCreatedNotification.targets)
class NewAircraftCreatedNotification < ApplicationNotification
deliver_by :database, if: :database_notifications?
deliver_by :email, mailer: "AircraftMailer", method: "created_email", if: :email_notifications?
# Variables that tells the system if it should send a notification if the recipient isn't subscribed
DEFAULT_SEND_NOTIFICATION = true
DEFAULT_SEND_EMAIL_NOTIFICATION = true
# Add required params
param :aircraft
def message
"#{params[:aircraft].name} has been created for use in the flight sim."
end
def url
aircraft_path(params[:aircraft])
end
def database_notifications?
DEFAULT_SEND_NOTIFICATION
end
def email_notifications?
DEFAULT_SEND_EMAIL_NOTIFICATION
end
def self.targets
# example of only sending notifications to admins. Really this just has to be an ActiveRecord::Relation of (likely) User objects.
User.admin
end
end
Email tracking and click tracking will be completed in #12
class AircraftMailer < NotificationMailer
before_action :set_user
before_action :set_aircraft
# use ahoy_messages to track all emails sent and track if their links are clicked.
# this isn't necessary off the start and can be implemented after the fact in the above linked issue.
track_clicks campaign: "aircraft_created_email", only: %i[created_email]
has_history extra: -> { {sent: true, messageable: @aircraft} }
def created_email
mail(to: @user.email, subject: "[DIGITAL FLIGHT SIM] New Aircraft Created: #{@aircraft.name}")
end
private
def set_user
@user = params[:recipient]
end
def set_aircraft
@aircraft = params[:aircraft]
end
end
class User < ApplicationRecord
has_many :notifications, as: :recipient, dependent: :destroy
# Helper for associating and destroying Notification records where(params: {object: self}).
has_noticed_notifications
has_noticed_notifications param_name: :assigned_to, destroy: false, model_name: "Notification"
has_noticed_notifications param_name: :requester, destroy: false, model_name: "Notification"
has_noticed_notifications param_name: :granted_by, destroy: false, model_name: "Notification"
end
<%# views/aircrafts_mailer/created_email %>
<p>Hello <%= @user.full_name %>,</p>
<br/>
<p>Aircraft <%= @aircraft %> has been created in the flight simulator.</p>
<br/>
<%= link_to "Check it out here", aircraft_url(@aircraft) %>
We use letter_opener to preview emails locally.
@jonathanloos are we required to make any snapshots of the DB before we run the migrations? How are our DB backups structured (on the app).
Yup our app's db is backed up with snapshots so there's no need to worry about us doing them. However, a rollback strategy without the backups (if possible) would be good to consider.
Looks good - assuming we make sure we update this doc with any snags/workarounds we hit when actually do https://github.com/pJeyakumar/noticed_upgrade_project/issues/3
@jonathanloos Are you using the AWS Aurora snapshots (every 5 minutes?), out of curiosity? Or what's the frequency of backup? Also, when's the last time you tested restoring from a backup, just wondering?
@pJeyakumar will the rake migration task cause downtime? How long would we expect it to last - I guess staging/dev will allow us to test realistically?
How will we validate we haven't lost any notifications in the process, on prod? Making sure row counts match?
@srb- the rake migration shouldn't cause any downtime given that we do our work in 2 distinct tasks.
type
value of these notifications are changed from Notification
to Notifier
) in our first deployment.
To answer your second question:
@pJeyakumar awesome, no downtime. Is there any concern with a notification being created in the old table while the migration is going on, and it gets missed? (e.g. any table locking, or after hours deployment, needed?)
You mentioned the app will support both old and new Notifications at the same time. How will we toggle that? Feature flag (do we use those?) or through a new deployment?
Looks good - assuming we make sure we update this doc with any snags/workarounds we hit when actually do #3
@jonathanloos Are you using the AWS Aurora snapshots (every 5 minutes?), out of curiosity? Or what's the frequency of backup? Also, when's the last time you tested restoring from a backup, just wondering?
@pJeyakumar will the rake migration task cause downtime? How long would we expect it to last - I guess staging/dev will allow us to test realistically?
How will we validate we haven't lost any notifications in the process, on prod? Making sure row counts match?
@srb- we haven't done a formal restoration in a long time, but the staging restoration is very similar to what we would do. I would need to connect with our AWS resource to check the procedure. As for the frequency I would also need to confirm that! (sorry not much answers).
@jonathanloos , For this upgrade, I understand that we may want to avoid downtime. However, considering a potential rollback situation, is there a good time during the day when we could run the upgrade if downtime becomes necessary? Additionally, knowing how long it would take to restore the previous database will help us estimate the required window of time.
@pJeyakumar awesome, no downtime. Is there any concern with a notification being created in the old table while the migration is going on, and it gets missed? (e.g. any table locking, or after hours deployment, needed?)
You mentioned the app will support both old and new Notifications at the same time. How will we toggle that? Feature flag (do we use those?) or through a new deployment?
With migrations of this size, we typically have a button (in our Developer settings page, which is on the app) to run the rake file in batches.
new_notification_created
, which we would set to true
once that notification's "new counterpart" has been created). We could use that new_notification_created
column to keep track of all notifications which don't have a "new counterpart" (which will help ensure that no notifications are missed).As for how the app will support both old and new notifications at the same time, we do not have Feature flags on the app, meaning we'd do it through a new deployment (my thinking is that our codebase should be able to support both the old/new notifications, if there is a noticeable difference between how we would handle both variations).
@pJeyakumar all looks good here, but it's important to note here that the tasks will not be run as a migration. Rather, it will execute as a job we trigger manually. This has a couple benefits:
For testing the migrations, remember we're dealing with over a million records. So for the rake task here we would want to have an idea of how long it will need to run for to pull & create all new records.
Will garner inspiration from the Noticed Update guide This issue #6 should also be worked on in tandem.
Game Plan
Option 1: ✅
Pros
Cons
Suggestion
Option 2: ❌
Pros
Cons
Noticed::Event
,Noticed::Coder
, etc).Changes to be made:
Create the following migration files via
rails g migration CreateNoticedTables
andrails g migration AddNotificationsCountToNoticedEvent
and copy the contents below into those corresponding files.rails db:migrate
to add the above tables to the database.noticed
gem in theGemfile
togem "noticed", "~> 2.3", ">= 2.3.2"
and runbundle install
Notification
model inapp/models/notification.rb
.app/notifications
folder toapp/notifiers
notification
classes to benotifier
by running the following code inrails console
dir = 'app/notifiers'
Dir.glob("#{dir}/*_notification.rb").each do |file| new_file = file.gsub('notification', 'notifier') FileUtils.mv(file, new_file) puts "Renamed: #{file} to #{new_file}" end
Notification
classes toNotifier
Notification <
and replace withNotifier <
LogbookEntryUpdatedNotification
becomesLogbookEntryUpdatedNotifier
<%= render notification %>
will have to change to<%= render partial: "notification", locals: {notification: notification} %>
notifier
classes with a new blocknotification_methods do … end
has_many :notifications, as: :recipient, dependent: :destroy
relationship tohas_many :noticed_notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification"
has_many_notifications
tohas_many :noticed_events, as: :record, dependent: :destroy, class_name: "Noticed::Event"
has_many :noticed_events, as: :record, dependent: :destroy, class_name: "Noticed::Event"
to theuser.rb
modeldeliver
todeliver_later
.deliver_later
for better clarity.param
andparams
in thenotifier
classes torequired_param
andrequired_params
respectively.param
with EXACT MATCHING enabled and for the FILES TO INCLUDE search for*_notifier.rb
and change torequired_param
params
(there is a SPACE in after "params") with EXACT MATCHING enabled and for the FILES TO INCLUDE search for*_notifier.rb
and change torequired_params
mark_as_read!
tomark_as_read
andmark_as_unread!
tomark_as_unread
.Noticed::Base
: toNoticed::Event
.deliver_by :database
from notifiers.method
is now a required option. Previously, it was inferred from the notification name but we've decided it would be better to be explicit.deliver_by :email, mailer: "TestMailer"
intest_notification.rb
must be changed todeliver_by :email, mailer: "TestMailer", method: "test"
where the method "test" is a function intest_mailer.rb
.To migrate the data into new tables:
job
to loop through all existing notifications and create a new record for each one.app/jobs/migrate_notifications_job.rb
) like this one:ADJUST BATCH_SIZE AND LIMIT AS NEEDED
BATCH_SIZE = 10 LIMIT = 100
Define the Notification model to access the old table
class Notification < ApplicationRecord self.inheritance_column = nil end
def perform(*args)
total_processed = 0
last_processed_at = get_last_processed_at
Process notifications in batches
Notifications are ordered by created_at date (newest first)
We then scope them to only included notifications OLDER than our last_processed_at notification
Then we run them in batches, in order of PRIMARY KEY descending (should be the same as created_at desc), find_in_batches MUST be ordered by PRIMARY KEY (we can choose either ascending or descending order).
Notification.order(created_at: :desc).where("created_at < ?", last_processed_at).limit(LIMIT).find_in_batches(order: :desc, batch_size: BATCH_SIZE) do |batch| batch.each do |notification| return if total_processed >= LIMIT migrate_notification(notification) total_processed += 1
end
end end
private
def get_last_processed_at last_event = Noticed::Event.order(created_at: :desc).last last_event ? last_event.created_at : DateTime.now end
def migrate_notification(notification)
Ensure the record has not already been migrated, Not sure about this query working in "Notice::Event", but something like this.
return if Noticed::Event.where(created_at: notification.created_at, params: Noticed::Coder.load(notification.params).with_indifferent_access).exists?
attributes = notification.attributes.slice("type", "created_at", "updated_at").with_indifferent_access attributes[:type] = attributes[:type].sub("Notification", "Notifier") attributes[:params] = Noticed::Coder.load(notification.params) attributes[:params] = {} if attributes[:params].try(:has_key?, "noticed_error") # Skip invalid records
attributes[:notifications_attributes] = [{ type: "#{attributes[:type]}::Notification", recipient_type: notification.recipient_type, recipient_id: notification.recipient_id, read_at: notification.read_at, seen_at: notification.read_at, created_at: notification.created_at, updated_at: notification.updated_at }]
Noticed::Event.create!(attributes) end end