2017-04-06 13:03:07 -04:00
|
|
|
# When a user is destroyed, some of their associated records are
|
|
|
|
# moved to a "Ghost User", to prevent these associated records from
|
|
|
|
# being destroyed.
|
|
|
|
#
|
|
|
|
# For example, all the issues/MRs a user has created are _not_ destroyed
|
|
|
|
# when the user is destroyed.
|
|
|
|
module Users
|
|
|
|
class MigrateToGhostUserService
|
|
|
|
extend ActiveSupport::Concern
|
|
|
|
|
|
|
|
attr_reader :ghost_user, :user
|
|
|
|
|
|
|
|
def initialize(user)
|
|
|
|
@user = user
|
|
|
|
end
|
|
|
|
|
|
|
|
def execute
|
Move records to the ghost user in a transaction.
- While deleting a user, some of the user's associated records are moved to the
ghost user so they aren't deleted. The user is blocked before these records
are moved, to prevent the user from creating new records while the migration
is happening, and so preventing a data race.
- Previously, if the migration failed, the user would _remain_ blocked, which is
not the expected behavior. On the other hand, we can't just stick the block +
migration into a transaction, because we want the block to be committed before
the migration starts (for the data race reason mentioned above).
- One solution (implemented in this commit) is to block the user in a parent
transaction, migrate the associated records in a nested sub-transaction, and
then unblock the user in the parent transaction if the sub-transaction fails.
2017-04-21 01:55:24 -04:00
|
|
|
transition = user.block_transition
|
2017-04-06 13:03:07 -04:00
|
|
|
|
|
|
|
user.transaction do
|
Move records to the ghost user in a transaction.
- While deleting a user, some of the user's associated records are moved to the
ghost user so they aren't deleted. The user is blocked before these records
are moved, to prevent the user from creating new records while the migration
is happening, and so preventing a data race.
- Previously, if the migration failed, the user would _remain_ blocked, which is
not the expected behavior. On the other hand, we can't just stick the block +
migration into a transaction, because we want the block to be committed before
the migration starts (for the data race reason mentioned above).
- One solution (implemented in this commit) is to block the user in a parent
transaction, migrate the associated records in a nested sub-transaction, and
then unblock the user in the parent transaction if the sub-transaction fails.
2017-04-21 01:55:24 -04:00
|
|
|
# Block the user before moving records to prevent a data race.
|
|
|
|
# For example, if the user creates an issue after `migrate_issues`
|
|
|
|
# runs and before the user is destroyed, the destroy will fail with
|
|
|
|
# an exception.
|
|
|
|
user.block
|
|
|
|
|
|
|
|
# Reverse the user block if record migration fails
|
|
|
|
if !migrate_records && transition
|
|
|
|
transition.rollback
|
|
|
|
user.save!
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
user.reload
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def migrate_records
|
|
|
|
user.transaction(requires_new: true) do
|
2017-04-06 13:03:07 -04:00
|
|
|
@ghost_user = User.ghost
|
|
|
|
|
|
|
|
migrate_issues
|
|
|
|
migrate_merge_requests
|
|
|
|
migrate_notes
|
|
|
|
migrate_abuse_reports
|
Move records to the ghost user in a transaction.
- While deleting a user, some of the user's associated records are moved to the
ghost user so they aren't deleted. The user is blocked before these records
are moved, to prevent the user from creating new records while the migration
is happening, and so preventing a data race.
- Previously, if the migration failed, the user would _remain_ blocked, which is
not the expected behavior. On the other hand, we can't just stick the block +
migration into a transaction, because we want the block to be committed before
the migration starts (for the data race reason mentioned above).
- One solution (implemented in this commit) is to block the user in a parent
transaction, migrate the associated records in a nested sub-transaction, and
then unblock the user in the parent transaction if the sub-transaction fails.
2017-04-21 01:55:24 -04:00
|
|
|
migrate_award_emojis
|
2017-04-06 13:03:07 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def migrate_issues
|
|
|
|
user.issues.update_all(author_id: ghost_user.id)
|
|
|
|
end
|
|
|
|
|
|
|
|
def migrate_merge_requests
|
|
|
|
user.merge_requests.update_all(author_id: ghost_user.id)
|
|
|
|
end
|
|
|
|
|
|
|
|
def migrate_notes
|
|
|
|
user.notes.update_all(author_id: ghost_user.id)
|
|
|
|
end
|
|
|
|
|
|
|
|
def migrate_abuse_reports
|
|
|
|
user.reported_abuse_reports.update_all(reporter_id: ghost_user.id)
|
|
|
|
end
|
|
|
|
|
Move records to the ghost user in a transaction.
- While deleting a user, some of the user's associated records are moved to the
ghost user so they aren't deleted. The user is blocked before these records
are moved, to prevent the user from creating new records while the migration
is happening, and so preventing a data race.
- Previously, if the migration failed, the user would _remain_ blocked, which is
not the expected behavior. On the other hand, we can't just stick the block +
migration into a transaction, because we want the block to be committed before
the migration starts (for the data race reason mentioned above).
- One solution (implemented in this commit) is to block the user in a parent
transaction, migrate the associated records in a nested sub-transaction, and
then unblock the user in the parent transaction if the sub-transaction fails.
2017-04-21 01:55:24 -04:00
|
|
|
def migrate_award_emojis
|
2017-04-06 13:03:07 -04:00
|
|
|
user.award_emoji.update_all(user_id: ghost_user.id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|