Fix delayed job errors related to follows #1621 (#1835)

* Add delayed_job_web to dev environment for debugging

* Add specs for the current Follow callbacks implementation

* Move follower touching to ActiveJob

* Spec for the touch followers job

* Move Follow#create_chat_channel to ActiveJob and make the job safe

* Add ActiveJob to send email notifications about follows

* Enqueue SendEmailNotificationJob after the follow is created

* Specs to wnsure jobs are enqueued on Follow creation

* Make CreateChatChannelJob queue name more specific
This commit is contained in:
Anna Buianova 2019-02-24 05:16:20 +03:00 committed by Ben Halpern
parent a36fb04d9e
commit 58b2013377
13 changed files with 260 additions and 9 deletions

View file

@ -108,6 +108,7 @@ group :development do
gem "brakeman", "~> 4.4", require: false
gem "bullet", "~> 5.9"
gem "bundler-audit", "~> 0.6"
gem "delayed_job_web", "~> 1.4"
gem "derailed_benchmarks", "~> 1.3"
gem "guard", "~> 2.15", require: false
gem "guard-livereload", "~> 2.5", require: false

View file

@ -235,6 +235,11 @@ GEM
delayed_job_active_record (4.1.3)
activerecord (>= 3.0, < 5.3)
delayed_job (>= 3.0, < 5)
delayed_job_web (1.4.3)
activerecord (> 3.0.0)
delayed_job (> 2.0.3)
rack-protection (>= 1.5.5)
sinatra (>= 1.4.4)
derailed (0.1.0)
derailed_benchmarks
derailed_benchmarks (1.3.5)
@ -969,6 +974,7 @@ DEPENDENCIES
dalli (~> 2.7)
database_cleaner (~> 1.7)
delayed_job_active_record (~> 4.1)
delayed_job_web (~> 1.4)
derailed (~> 0.1)
derailed_benchmarks (~> 1.3)
devise (~> 4.6)

View file

@ -0,0 +1,12 @@
module Follows
class CreateChatChannelJob < ApplicationJob
queue_as :create_chat_channel_after_follow
def perform(follow_id)
follow = Follow.includes(:follower, :followable).find_by(id: follow_id, follower_type: "User", followable_type: "User")
return unless follow&.followable&.following?(follow.follower)
ChatChannel.create_with_users([follow.followable, follow.follower])
end
end
end

View file

@ -0,0 +1,15 @@
module Follows
class SendEmailNotificationJob < ApplicationJob
queue_as :send_follow_email_notification
def perform(follow_id, mailer = NotifyMailer)
follow = Follow.find_by(id: follow_id, followable_type: "User")
return unless follow&.followable&.email? && follow.followable.email_follower_notifications
return if EmailMessage.where(user_id: follow.followable_id).
where("sent_at > ?", rand(15..35).hours.ago).
where("subject LIKE ?", "%followed you on dev.to%").any?
mailer.new_follower_email(follow).deliver
end
end
end

View file

@ -0,0 +1,13 @@
module Follows
class TouchFollowerJob < ApplicationJob
queue_as :touch_follower
def perform(follow_id)
follow = Follow.find_by(id: follow_id)
return unless follow
follow.follower.touch
follow.follower.touch(:last_followed_at)
end
end
end

View file

@ -20,16 +20,31 @@ class Follow < ApplicationRecord
["follows.followable_type = ?", "Organization"] => "following_orgs_count",
["follows.followable_type = ?", "ActsAsTaggableOn::Tag"] => "following_tags_count"
}
after_save :touch_user
after_save :touch_user_followed_at
after_create :send_email_notification
after_create :create_chat_channel
after_save :touch_follower
after_create :send_email_notification, :create_chat_channel
before_destroy :modify_chat_channel_status
validates :followable_id, uniqueness: { scope: %i[followable_type follower_id] }
private
def touch_follower
Follows::TouchFollowerJob.perform_later(id)
end
def create_chat_channel
return unless followable_type == "User"
Follows::CreateChatChannelJob.perform_later(id)
end
def send_email_notification
return unless followable.class.name == "User" && followable.email?
Follows::SendEmailNotificationJob.perform_later(id)
end
# TODO: remove methods #touch_user, #touch_user_followed_at, #create_chat_channel_without_delay, #send_email_notification_without_delay
def touch_user
follower.touch
end
@ -40,14 +55,14 @@ class Follow < ApplicationRecord
end
handle_asynchronously :touch_user_followed_at
def create_chat_channel
# *_without_delay method will be used if there're jobs created before introducing ActiveJob
def create_chat_channel_without_delay
if followable_type == "User" && followable.following?(follower)
ChatChannel.create_with_users([followable, follower])
end
end
handle_asynchronously :create_chat_channel
def send_email_notification
def send_email_notification_without_delay
if followable.class.name == "User" && followable.email.present? && followable.email_follower_notifications
return if EmailMessage.where(user_id: followable.id).
where("sent_at > ?", rand(15..35).hours.ago).
@ -56,7 +71,6 @@ class Follow < ApplicationRecord
NotifyMailer.new_follower_email(self).deliver
end
end
handle_asynchronously :send_email_notification
def modify_chat_channel_status
if followable_type == "User" && followable.following?(follower)

View file

@ -1,4 +1,4 @@
Delayed::Worker.destroy_failed_jobs = true
Delayed::Worker.destroy_failed_jobs = !Rails.env.development?
Delayed::Worker.sleep_delay = 60
Delayed::Worker.max_attempts = 10
Delayed::Worker.max_run_time = 30.minutes

View file

@ -7,6 +7,10 @@ Rails.application.routes.draw do
registrations: "registrations"
}
if Rails.env.development?
match "/delayed_job" => DelayedJobWeb, :anchor => false, :via => [:get, :post]
end
devise_scope :user do
delete "/sign_out" => "devise/sessions#destroy"
get "/enter" => "registrations#new", as: :new_user_registration_path

6
spec/factories/follow.rb Normal file
View file

@ -0,0 +1,6 @@
FactoryBot.define do
factory :follow do
association :follower, factory: :user
association :followable, factory: :organization
end
end

View file

@ -0,0 +1,42 @@
require "rails_helper"
RSpec.describe Follows::CreateChatChannelJob, type: :job do
describe "#perform_later" do
it "enqueues the job" do
ActiveJob::Base.queue_adapter = :test
expect do
described_class.perform_later(3)
end.to have_enqueued_job.with(3).on_queue("create_chat_channel_after_follow")
end
end
describe "#perform_now" do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:follow) { create(:follow, follower: user, followable: user2) }
it "creates a chat channel when mutual followers" do
follow2 = create(:follow, follower: user2, followable: user)
expect do
described_class.perform_now(follow2.id)
end.to change(ChatChannel, :count).by(1)
end
it "doesn't create a chat channel when the follow is not mutual" do
expect do
described_class.perform_now(follow.id)
end.not_to change(ChatChannel, :count)
end
it "doesn't fail if follow doesn't exist" do
described_class.perform_now(Follow.maximum(:id).to_i + 1)
end
it "doesn't do anything if follow is not from user to user" do
org_follow = create(:follow, follower: user, followable: create(:organization))
expect do
described_class.perform_now(org_follow.id)
end.not_to change(ChatChannel, :count)
end
end
end

View file

@ -0,0 +1,46 @@
require "rails_helper"
RSpec.describe Follows::SendEmailNotificationJob, type: :job do
describe "#perform_later" do
it "enqueues the job" do
ActiveJob::Base.queue_adapter = :test
expect do
described_class.perform_later(3)
end.to have_enqueued_job.with(3).on_queue("send_follow_email_notification")
end
end
describe "#perform_now" do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:follow) { create(:follow, follower: user, followable: user2) }
let(:mailer) { double }
before do
deliverer = double
allow(deliverer).to receive(:deliver)
allow(mailer).to receive(:new_follower_email).and_return(deliverer)
end
it "sends a new_follower_email" do
user2.update_column(:email_follower_notifications, true)
described_class.new(follow.id, mailer).perform_now
expect(mailer).to have_received(:new_follower_email).once
end
it "doesn't create an EmailMessage if it already exists" do
EmailMessage.create!(user_id: user2.id, sent_at: Time.now, subject: "#{user.username} followed you on dev.to")
described_class.new(follow.id, mailer).perform_now
expect(mailer).not_to have_received(:new_follower_email)
end
it "doesn't send an email if user has disabled notifications" do
user2.update_column(:email_follower_notifications, false)
expect(mailer).not_to have_received(:new_follower_email)
end
it "doesn't fail if follow doesn't exist" do
described_class.perform_now(Follow.maximum(:id).to_i + 1)
end
end
end

View file

@ -0,0 +1,29 @@
require "rails_helper"
RSpec.describe Follows::TouchFollowerJob, type: :job do
describe "#perform_later" do
it "enqueues the job" do
ActiveJob::Base.queue_adapter = :test
expect do
described_class.perform_later(3)
end.to have_enqueued_job.with(3).on_queue("touch_follower")
end
it "touches a follower" do
user = create(:user)
user.update_columns(updated_at: Time.now - 1.day, last_followed_at: Time.now - 1.day)
now = Time.now
follow = create(:follow, follower: user)
described_class.perform_now(follow.id)
user.reload
expect(user.updated_at).to be >= now
expect(user.last_followed_at).to be >= now
end
it "doesn't fail if follow doesn't exist" do
described_class.perform_now(Follow.maximum(:id).to_i + 1)
end
end
end

View file

@ -8,4 +8,67 @@ RSpec.describe Follow, type: :model do
user.follow(user_2)
expect(user.following?(user_2)).to eq(true)
end
context "when enqueuing jobs" do
before { ActiveJob::Base.queue_adapter = :test }
it "enqueues touch follower job on creation" do
expect do
Follow.create(follower: user, followable: user_2)
end.to have_enqueued_job(Follows::TouchFollowerJob)
end
it "enqueues create channel job" do
expect do
Follow.create(follower: user, followable: user_2)
end.to have_enqueued_job(Follows::CreateChatChannelJob)
end
it "enqueues send notification job" do
expect do
Follow.create(follower: user, followable: user_2)
end.to have_enqueued_job(Follows::SendEmailNotificationJob)
end
end
context "when creating and inline" do
before { ActiveJob::Base.queue_adapter = :inline }
it "touches the follower user while creating" do
user.update_columns(updated_at: Time.now - 1.day, last_followed_at: Time.now - 1.day)
now = Time.now
Follow.create!(follower: user, followable: user_2)
user.reload
expect(user.updated_at).to be >= now
expect(user.last_followed_at).to be >= now
end
it "doesn't create a channel when a followable is an org" do
expect do
Follow.create!(follower: user, followable: create(:organization))
end.not_to change(ChatChannel, :count)
end
it "doesn't create a chat channel when users don't follow mutually" do
expect do
Follow.create!(follower: user, followable: user_2)
end.not_to change(ChatChannel, :count)
end
it "creates a chat channel when users follow mutually" do
Follow.create!(follower: user_2, followable: user)
expect do
Follow.create!(follower: user, followable: user_2)
end.to change(ChatChannel, :count).by(1)
end
it "sends an email notification" do
user_2.update_column(:email_follower_notifications, true)
expect do
run_background_jobs_immediately do
Follow.create!(follower: user, followable: user_2)
end
end.to change(EmailMessage, :count).by(1)
end
end
end