DEV: ensure rebaking works even when some users have inconsistent data (#30261)

* DEV: add db consistency check for UserEmail

* DEV: add db consistency check for UserAvatar

* DEV: ignore inconsistent data related to user avatars when deciding whether to rebake old posts


Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>

---------

Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
This commit is contained in:
Kelv 2024-12-16 19:48:25 +08:00 committed by GitHub
parent ce8c2ef6d9
commit 04ba5baec0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 118 additions and 2 deletions

View File

@ -22,6 +22,7 @@ module Jobs
CategoryTagStat,
User,
UserAvatar,
UserEmail,
Category,
TopicThumbnail,
].each do |klass|

View File

@ -25,7 +25,10 @@ module Jobs
# Forces rebake of old posts where needed, as long as no system avatars need updating
if !SiteSetting.automatically_download_gravatars ||
!UserAvatar.where("last_gravatar_download_attempt IS NULL").limit(1).first
!UserAvatar
.joins(user: :user_emails)
.where(user_emails: { primary: true }, last_gravatar_download_attempt: nil)
.exists?
problems = Post.rebake_old(SiteSetting.rebake_old_posts_count, priority: :ultra_low)
problems.each do |hash|
post_id = hash[:post].id

View File

@ -152,6 +152,13 @@ class UserAvatar < ActiveRecord::Base
end
def self.ensure_consistency!(max_optimized_avatars_to_remove: 20_000)
DB.exec <<~SQL
DELETE FROM user_avatars
USING user_avatars ua
LEFT JOIN users u ON ua.user_id = u.id
WHERE user_avatars.id = ua.id AND u.id IS NULL
SQL
DB.exec <<~SQL
UPDATE user_avatars
SET gravatar_upload_id = NULL

View File

@ -22,6 +22,23 @@ class UserEmail < ActiveRecord::Base
before_save -> { destroy_email_tokens(self.email_was) }, if: :will_save_change_to_email?
after_destroy { destroy_email_tokens(self.email) }
def self.ensure_consistency!
user_ids_without_primary_email = DB.query_single <<~SQL
SELECT u.id
FROM users u
LEFT JOIN user_emails ue ON u.id = ue.user_id AND ue.primary = true
WHERE ue.id IS NULL;
SQL
user_ids_without_primary_email.each do |user_id|
UserEmail.create!(
user_id: user_id,
# 64 max character length of local-part for the email address https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.1
email: "#{SecureRandom.alphanumeric(64)}@missing-primary-email.invalid",
primary: true,
)
end
end
def normalize_email
self.normalized_email =

View File

@ -1,6 +1,13 @@
# frozen_string_literal: true
RSpec.describe Jobs::PeriodicalUpdates do
fab!(:admin)
before do
UserAvatar.where(last_gravatar_download_attempt: nil).update_all(
last_gravatar_download_attempt: Time.now,
)
end
it "works" do
# does not blow up, no mocks, everything is called
Jobs::PeriodicalUpdates.new.execute(nil)
@ -8,7 +15,7 @@ RSpec.describe Jobs::PeriodicalUpdates do
it "can rebake old posts when automatically_download_gravatars is false" do
SiteSetting.automatically_download_gravatars = false
post = create_post
post = create_post(user: admin)
post.update_columns(baked_at: Time.new(2000, 1, 1), baked_version: -1)
Sidekiq::Testing.fake! do
@ -31,4 +38,52 @@ RSpec.describe Jobs::PeriodicalUpdates do
post.reload
expect(post.baked_at).to eq_time(baked)
end
it "does not rebake old posts when automatically_download_gravatars is true and a valid user avatar needs updating" do
SiteSetting.automatically_download_gravatars = true
UserAvatar.last.update!(last_gravatar_download_attempt: nil)
post = create_post(user: admin)
post.update_columns(baked_at: Time.new(2000, 1, 1), baked_version: -1)
Sidekiq::Testing.fake! do
Jobs::ProcessPost.jobs.clear
Jobs::PeriodicalUpdates.new.execute
expect(Jobs::ProcessPost.jobs).to be_empty
end
end
it "does not rebake old posts when there are user avatars that need updating" do
SiteSetting.automatically_download_gravatars = true
post = create_post(user: admin)
post.update_columns(baked_at: Time.new(2000, 1, 1), baked_version: -1)
UserAvatar.last.update!(last_gravatar_download_attempt: nil)
Sidekiq::Testing.fake! do
Jobs::ProcessPost.jobs.clear
Jobs::PeriodicalUpdates.new.execute
expect(Jobs::ProcessPost.jobs).to be_empty
end
end
# inconsistent data will be fixed by ensure_consistency! of the relevant models
it "rebakes old posts when there are user avatars that need updating but have inconsistent data" do
SiteSetting.automatically_download_gravatars = true
user_avatar_without_user = Fabricate(:user_avatar, last_gravatar_download_attempt: Time.now)
user_avatar_without_user.user.delete
user_without_any_email = Fabricate(:user)
user_without_any_email.user_emails.delete_all
user_without_primary_email = Fabricate(:user)
user_without_primary_email.primary_email.update_column(:primary, false)
post = create_post(user: admin)
post.update_columns(baked_at: Time.new(2000, 1, 1), baked_version: -1)
Sidekiq::Testing.fake! do
Jobs::ProcessPost.jobs.clear
Jobs::PeriodicalUpdates.new.execute
expect(Jobs::ProcessPost.jobs.length).to eq(1)
end
end
end

View File

@ -234,5 +234,16 @@ RSpec.describe UserAvatar do
expect(user_avatar.gravatar_upload_id).to eq(nil)
expect(user_avatar.custom_upload_id).to eq(nil)
end
it "deletes avatars without users and does not remove avatars with users" do
user_avatar_with_user = Fabricate(:user_avatar)
user_avatar_without_user = Fabricate(:user_avatar)
user_avatar_without_user.user.delete
UserAvatar.ensure_consistency!
expect(UserAvatar.exists?(user_avatar_with_user.id)).to eq true
expect(UserAvatar.exists?(user_avatar_without_user.id)).to eq false
end
end
end

View File

@ -63,4 +63,26 @@ RSpec.describe UserEmail do
expect(user.user_emails.count).to eq 3
end
end
describe ".ensure_consistency!" do
context "when some users have no primary emails" do
it "creates primary emails for the users without a primary email" do
user_with_primary_email = Fabricate(:user)
user_without_primary_email = Fabricate(:user)
user_without_any_email = Fabricate(:user)
user_without_primary_email.primary_email.update_column(:primary, false)
user_without_any_email.user_emails.delete_all
original_email_of_user_with_primary_email = user_with_primary_email.primary_email.email
described_class.ensure_consistency!
expect(user_without_primary_email.reload.primary_email).to be_present
expect(user_without_any_email.reload.primary_email).to be_present
expect(
user_with_primary_email.reload.primary_email.email,
).to eq original_email_of_user_with_primary_email
end
end
end
end