Commit 0fdfb125 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Block user outside transaction when deleting

When migrating user records to the ghost user, we block the user outside
the transaction so that the user cannot create any new records.

This also avoids having a subtransaction.
parent ffedc13c
...@@ -14,23 +14,30 @@ module Users ...@@ -14,23 +14,30 @@ module Users
def initialize(user) def initialize(user)
@user = user @user = user
@ghost_user = User.ghost
end end
def execute def execute
transition = user.block_transition transition = user.block_transition
user.transaction do # Block the user before moving records to prevent a data race.
# Block the user before moving records to prevent a data race. # For example, if the user creates an issue after `migrate_issues`
# For example, if the user creates an issue after `migrate_issues` # runs and before the user is destroyed, the destroy will fail with
# runs and before the user is destroyed, the destroy will fail with # an exception.
# an exception. user.block
user.block
begin
user.transaction do
migrate_records
end
rescue Exception # rubocop:disable Lint/RescueException
# Reverse the user block if record migration fails # Reverse the user block if record migration fails
if !migrate_records_in_transaction && transition if transition
transition.rollback transition.rollback
user.save! user.save!
end end
raise
end end
user.reset user.reset
...@@ -38,14 +45,6 @@ module Users ...@@ -38,14 +45,6 @@ module Users
private private
def migrate_records_in_transaction
user.transaction(requires_new: true) do
@ghost_user = User.ghost
migrate_records
end
end
def migrate_records def migrate_records
migrate_issues migrate_issues
migrate_merge_requests migrate_merge_requests
......
...@@ -92,23 +92,5 @@ RSpec.describe Users::MigrateToGhostUserService do ...@@ -92,23 +92,5 @@ RSpec.describe Users::MigrateToGhostUserService do
let(:created_record) { create(:review, author: user) } let(:created_record) { create(:review, author: user) }
end end
end end
context "when record migration fails with a rollback exception" do
before do
expect_any_instance_of(ActiveRecord::Associations::CollectionProxy)
.to receive(:update_all).and_raise(ActiveRecord::Rollback)
end
context "for records that were already migrated" do
let!(:issue) { create(:issue, project: project, author: user) }
let!(:merge_request) { create(:merge_request, source_project: project, author: user, target_branch: "first") }
it "reverses the migration" do
service.execute
expect(issue.reload.author).to eq(user)
end
end
end
end end
end end
...@@ -32,7 +32,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos ...@@ -32,7 +32,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos
expect(user).to be_blocked expect(user).to be_blocked
end end
it 'migrates all associated fields to te "Ghost user"' do it 'migrates all associated fields to the "Ghost user"' do
service.execute service.execute
migrated_record = record_class.find_by_id(record.id) migrated_record = record_class.find_by_id(record.id)
...@@ -46,40 +46,19 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos ...@@ -46,40 +46,19 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos
context "when #{record_class_name} migration fails and is rolled back" do context "when #{record_class_name} migration fails and is rolled back" do
before do before do
expect_any_instance_of(ActiveRecord::Associations::CollectionProxy) expect_any_instance_of(ActiveRecord::Associations::CollectionProxy)
.to receive(:update_all).and_raise(ActiveRecord::Rollback) .to receive(:update_all).and_raise(ActiveRecord::StatementTimeout)
end end
it 'rolls back the user block' do it 'rolls back the user block' do
service.execute expect { service.execute }.to raise_error(ActiveRecord::StatementTimeout)
expect(user.reload).not_to be_blocked expect(user.reload).not_to be_blocked
end end
it "doesn't unblock an previously-blocked user" do it "doesn't unblock a previously-blocked user" do
user.block user.block
service.execute expect { service.execute }.to raise_error(ActiveRecord::StatementTimeout)
expect(user.reload).to be_blocked
end
end
context "when #{record_class_name} migration fails with a non-rollback exception" do
before do
expect_any_instance_of(ActiveRecord::Associations::CollectionProxy)
.to receive(:update_all).and_raise(ArgumentError)
end
it 'rolls back the user block' do
service.execute rescue nil
expect(user.reload).not_to be_blocked
end
it "doesn't unblock an previously-blocked user" do
user.block
service.execute rescue nil
expect(user.reload).to be_blocked expect(user.reload).to be_blocked
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment