mirror of
https://github.com/discourse/discourse.git
synced 2025-01-30 05:05:15 +08:00
PERF: Do not enqueue digest emails when attempted recently (#10849)
Previously, Jobs::EnqueueDigestEmails would enqueue a digest job for every user, even if there are no topics to send. The digest job would exit, no email would send, and last_emailed_at would not change. 30 minutes later, Jobs::EnqueueDigestEmails would run again and re-enqueue jobs for the same users.120fa8ad
introduced a temporary mitigation for this issue, by randomly selecting a subset of those users each time. This commit adds a new `digest_attempted_at` column to the `user_stats` table. This column is updated every time a digest job completes for a user. Using this, we can avoid scheduling digest jobs for the same user every 30 minutes. This also removes the random user selection in120fa8ad
, and instead prioritizes users who had digests attempted the longest time ago.
This commit is contained in:
parent
68d4b92bba
commit
c0293339b8
|
@ -22,6 +22,15 @@ module Jobs
|
||||||
# of extra work when emails are disabled.
|
# of extra work when emails are disabled.
|
||||||
return if quit_email_early?
|
return if quit_email_early?
|
||||||
|
|
||||||
|
send_user_email(args)
|
||||||
|
|
||||||
|
if args[:user_id].present? && args[:type] == :digest
|
||||||
|
# Record every attempt at sending a digest email, even if it was skipped
|
||||||
|
UserStat.where(user_id: args[:user_id]).update_all(digest_attempted_at: Time.zone.now)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_user_email(args)
|
||||||
post = nil
|
post = nil
|
||||||
notification = nil
|
notification = nil
|
||||||
type = args[:type]
|
type = args[:type]
|
||||||
|
|
|
@ -6,13 +6,9 @@ module Jobs
|
||||||
every 30.minutes
|
every 30.minutes
|
||||||
|
|
||||||
def execute(args)
|
def execute(args)
|
||||||
return if SiteSetting.disable_digest_emails? || SiteSetting.private_email?
|
return if SiteSetting.disable_digest_emails? || SiteSetting.private_email? || SiteSetting.disable_emails == 'yes'
|
||||||
users = target_user_ids
|
users = target_user_ids
|
||||||
|
|
||||||
if users.length > GlobalSetting.max_digests_enqueued_per_30_mins_per_site
|
|
||||||
users = users.shuffle[0...GlobalSetting.max_digests_enqueued_per_30_mins_per_site]
|
|
||||||
end
|
|
||||||
|
|
||||||
users.each do |user_id|
|
users.each do |user_id|
|
||||||
::Jobs.enqueue(:user_email, type: :digest, user_id: user_id)
|
::Jobs.enqueue(:user_email, type: :digest, user_id: user_id)
|
||||||
end
|
end
|
||||||
|
@ -30,12 +26,16 @@ module Jobs
|
||||||
.where("user_stats.bounce_score < #{SiteSetting.bounce_score_threshold}")
|
.where("user_stats.bounce_score < #{SiteSetting.bounce_score_threshold}")
|
||||||
.where("user_emails.primary")
|
.where("user_emails.primary")
|
||||||
.where("COALESCE(last_emailed_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)")
|
.where("COALESCE(last_emailed_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)")
|
||||||
|
.where("COALESCE(user_stats.digest_attempted_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)")
|
||||||
.where("COALESCE(last_seen_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)")
|
.where("COALESCE(last_seen_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)")
|
||||||
.where("COALESCE(last_seen_at, '2010-01-01') >= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * #{SiteSetting.suppress_digest_email_after_days})")
|
.where("COALESCE(last_seen_at, '2010-01-01') >= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * #{SiteSetting.suppress_digest_email_after_days})")
|
||||||
|
.order("user_stats.digest_attempted_at ASC NULLS FIRST")
|
||||||
|
|
||||||
# If the site requires approval, make sure the user is approved
|
# If the site requires approval, make sure the user is approved
|
||||||
query = query.where("approved OR moderator OR admin") if SiteSetting.must_approve_users?
|
query = query.where("approved OR moderator OR admin") if SiteSetting.must_approve_users?
|
||||||
|
|
||||||
|
query = query.limit(GlobalSetting.max_digests_enqueued_per_30_mins_per_site)
|
||||||
|
|
||||||
query.pluck(:id)
|
query.pluck(:id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -290,4 +290,5 @@ end
|
||||||
# flags_ignored :integer default(0), not null
|
# flags_ignored :integer default(0), not null
|
||||||
# first_unread_at :datetime not null
|
# first_unread_at :datetime not null
|
||||||
# distinct_badge_count :integer default(0), not null
|
# distinct_badge_count :integer default(0), not null
|
||||||
|
# digest_attempted_at :datetime
|
||||||
#
|
#
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddDigestAttemptedAtToUserStats < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
add_column :user_stats, :digest_attempted_at, :timestamp
|
||||||
|
end
|
||||||
|
end
|
|
@ -122,16 +122,29 @@ describe Jobs::EnqueueDigestEmails do
|
||||||
let(:user) { Fabricate(:user) }
|
let(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
it "limits jobs enqueued per max_digests_enqueued_per_30_mins_per_site" do
|
it "limits jobs enqueued per max_digests_enqueued_per_30_mins_per_site" do
|
||||||
Fabricate(:user, last_seen_at: 2.months.ago, last_emailed_at: 2.months.ago)
|
user1 = Fabricate(:user, last_seen_at: 2.months.ago, last_emailed_at: 2.months.ago)
|
||||||
Fabricate(:user, last_seen_at: 2.months.ago, last_emailed_at: 2.months.ago)
|
user2 = Fabricate(:user, last_seen_at: 2.months.ago, last_emailed_at: 2.months.ago)
|
||||||
|
|
||||||
|
user1.user_stat.update(digest_attempted_at: 2.week.ago)
|
||||||
|
user2.user_stat.update(digest_attempted_at: 3.weeks.ago)
|
||||||
|
|
||||||
global_setting :max_digests_enqueued_per_30_mins_per_site, 1
|
global_setting :max_digests_enqueued_per_30_mins_per_site, 1
|
||||||
|
|
||||||
# I don't love fakes, but no point sending this fake email
|
expect_enqueued_with(job: :user_email, args: { type: :digest, user_id: user2.id }) do
|
||||||
Sidekiq::Testing.fake! do
|
expect { Jobs::EnqueueDigestEmails.new.execute(nil) }.to change(Jobs::UserEmail.jobs, :size).by (1)
|
||||||
expect do
|
end
|
||||||
Jobs::EnqueueDigestEmails.new.execute(nil)
|
|
||||||
end.to change(Jobs::UserEmail.jobs, :size).by (1)
|
# The job didn't actually run, so fake the user_stat update
|
||||||
|
user2.user_stat.update(digest_attempted_at: Time.zone.now)
|
||||||
|
|
||||||
|
expect_enqueued_with(job: :user_email, args: { type: :digest, user_id: user1.id }) do
|
||||||
|
expect { Jobs::EnqueueDigestEmails.new.execute(nil) }.to change(Jobs::UserEmail.jobs, :size).by (1)
|
||||||
|
end
|
||||||
|
|
||||||
|
user1.user_stat.update(digest_attempted_at: Time.zone.now)
|
||||||
|
|
||||||
|
expect_not_enqueued_with(job: :user_email) do
|
||||||
|
Jobs::EnqueueDigestEmails.new.execute(nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -42,17 +42,20 @@ describe Jobs::UserEmail do
|
||||||
|
|
||||||
context 'not emailed recently' do
|
context 'not emailed recently' do
|
||||||
before do
|
before do
|
||||||
|
freeze_time
|
||||||
user.update!(last_emailed_at: 8.days.ago)
|
user.update!(last_emailed_at: 8.days.ago)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "calls the mailer when the user exists" do
|
it "calls the mailer when the user exists" do
|
||||||
Jobs::UserEmail.new.execute(type: :digest, user_id: user.id)
|
Jobs::UserEmail.new.execute(type: :digest, user_id: user.id)
|
||||||
expect(ActionMailer::Base.deliveries).to_not be_empty
|
expect(ActionMailer::Base.deliveries).to_not be_empty
|
||||||
|
expect(user.user_stat.reload.digest_attempted_at).to eq_time(Time.zone.now)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'recently emailed' do
|
context 'recently emailed' do
|
||||||
before do
|
before do
|
||||||
|
freeze_time
|
||||||
user.update!(last_emailed_at: 2.hours.ago)
|
user.update!(last_emailed_at: 2.hours.ago)
|
||||||
user.user_option.update!(digest_after_minutes: 1.day.to_i / 60)
|
user.user_option.update!(digest_after_minutes: 1.day.to_i / 60)
|
||||||
end
|
end
|
||||||
|
@ -60,6 +63,7 @@ describe Jobs::UserEmail do
|
||||||
it 'skips sending digest email' do
|
it 'skips sending digest email' do
|
||||||
Jobs::UserEmail.new.execute(type: :digest, user_id: user.id)
|
Jobs::UserEmail.new.execute(type: :digest, user_id: user.id)
|
||||||
expect(ActionMailer::Base.deliveries).to eq([])
|
expect(ActionMailer::Base.deliveries).to eq([])
|
||||||
|
expect(user.user_stat.reload.digest_attempted_at).to eq_time(Time.zone.now)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue
Block a user