mirror of
https://github.com/discourse/discourse.git
synced 2025-02-21 18:25:25 +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 in 120fa8ad, 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.
|
||||
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
|
||||
notification = nil
|
||||
type = args[:type]
|
||||
|
@ -6,13 +6,9 @@ module Jobs
|
||||
every 30.minutes
|
||||
|
||||
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
|
||||
|
||||
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|
|
||||
::Jobs.enqueue(:user_email, type: :digest, user_id: user_id)
|
||||
end
|
||||
@ -30,12 +26,16 @@ module Jobs
|
||||
.where("user_stats.bounce_score < #{SiteSetting.bounce_score_threshold}")
|
||||
.where("user_emails.primary")
|
||||
.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 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
|
||||
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)
|
||||
end
|
||||
|
||||
|
@ -290,4 +290,5 @@ end
|
||||
# flags_ignored :integer default(0), not null
|
||||
# first_unread_at :datetime 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) }
|
||||
|
||||
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)
|
||||
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)
|
||||
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
|
||||
|
||||
# I don't love fakes, but no point sending this fake email
|
||||
Sidekiq::Testing.fake! do
|
||||
expect do
|
||||
Jobs::EnqueueDigestEmails.new.execute(nil)
|
||||
end.to change(Jobs::UserEmail.jobs, :size).by (1)
|
||||
expect_enqueued_with(job: :user_email, args: { type: :digest, user_id: user2.id }) do
|
||||
expect { Jobs::EnqueueDigestEmails.new.execute(nil) }.to change(Jobs::UserEmail.jobs, :size).by (1)
|
||||
end
|
||||
|
||||
# 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
|
||||
|
||||
|
@ -42,17 +42,20 @@ describe Jobs::UserEmail do
|
||||
|
||||
context 'not emailed recently' do
|
||||
before do
|
||||
freeze_time
|
||||
user.update!(last_emailed_at: 8.days.ago)
|
||||
end
|
||||
|
||||
it "calls the mailer when the user exists" do
|
||||
Jobs::UserEmail.new.execute(type: :digest, user_id: user.id)
|
||||
expect(ActionMailer::Base.deliveries).to_not be_empty
|
||||
expect(user.user_stat.reload.digest_attempted_at).to eq_time(Time.zone.now)
|
||||
end
|
||||
end
|
||||
|
||||
context 'recently emailed' do
|
||||
before do
|
||||
freeze_time
|
||||
user.update!(last_emailed_at: 2.hours.ago)
|
||||
user.user_option.update!(digest_after_minutes: 1.day.to_i / 60)
|
||||
end
|
||||
@ -60,6 +63,7 @@ describe Jobs::UserEmail do
|
||||
it 'skips sending digest email' do
|
||||
Jobs::UserEmail.new.execute(type: :digest, user_id: user.id)
|
||||
expect(ActionMailer::Base.deliveries).to eq([])
|
||||
expect(user.user_stat.reload.digest_attempted_at).to eq_time(Time.zone.now)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user