mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-08-03 16:04:30 +00:00
184 lines
7.0 KiB
Ruby
184 lines
7.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Gitlab
|
|
module Import
|
|
class SourceUserMapper
|
|
include Gitlab::ExclusiveLeaseHelpers
|
|
|
|
LRU_CACHE_SIZE = 100
|
|
LOCK_TTL = 15.seconds.freeze
|
|
LOCK_SLEEP = 0.3.seconds.freeze
|
|
LOCK_RETRIES = 100
|
|
|
|
DuplicatedUserError = Class.new(StandardError)
|
|
|
|
def initialize(namespace:, import_type:, source_hostname:)
|
|
@namespace = namespace.root_ancestor
|
|
@import_type = import_type
|
|
@source_hostname = Gitlab::UrlHelpers.normalized_base_url(source_hostname)
|
|
end
|
|
|
|
# Finds a source user by the provided `source_user_identifier`.
|
|
#
|
|
# This method first checks an in-memory LRU (Least Recently Used) cache,
|
|
# stored in `SafeRequestStore`, to avoid unnecessary database queries.
|
|
# If the source user is not present in the cache, it will query the database
|
|
# and store the result in the cache for future use.
|
|
#
|
|
# Since jobs may create source users concurrently, the ActiveRecord query
|
|
# cache is explicitly disabled when querying the database to ensure that
|
|
# we always get the latest data.
|
|
#
|
|
# @param [String] source_user_identifier The identifier for the source user to find.
|
|
# @return [Import::SourceUser, nil] The found source user object, or `nil` if no match is found.
|
|
def find_source_user(source_user_identifier)
|
|
cache_from_request_store[source_user_identifier] ||= ::Import::SourceUser.uncached do
|
|
source_user = ::Import::SourceUser.find_source_user(
|
|
source_user_identifier: source_user_identifier,
|
|
namespace: namespace,
|
|
source_hostname: source_hostname,
|
|
import_type: import_type
|
|
)
|
|
|
|
# If the record has no `#mapped_user_id`, the record would be unusuable for import.
|
|
# It can be in this state if the reassigned_to_user, or placeholder_user were deleted
|
|
# unexpectedly. We intentionally do not have a cascade delete association with
|
|
# users on this record as we do not want to have unmapped contributions be lost.
|
|
# In this situation we reset the record.
|
|
source_user = reset_source_user!(source_user) if reset_source_user?(source_user)
|
|
|
|
source_user
|
|
end
|
|
end
|
|
|
|
# Finds a source user by the provided `source_user_identifier` or creates a new one
|
|
def find_or_create_source_user(source_name:, source_username:, source_user_identifier:, cache: true)
|
|
source_user = find_source_user(source_user_identifier)
|
|
return source_user if source_user
|
|
|
|
source_user = create_source_user(
|
|
source_name: source_name,
|
|
source_username: source_username,
|
|
source_user_identifier: source_user_identifier
|
|
)
|
|
|
|
cache_from_request_store[source_user_identifier] = source_user if cache
|
|
|
|
source_user
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :namespace, :import_type, :source_hostname
|
|
|
|
def cache_from_request_store
|
|
Gitlab::SafeRequestStore[:source_user_cache] ||= LruRedux::Cache.new(LRU_CACHE_SIZE)
|
|
end
|
|
|
|
def create_source_user(source_name:, source_username:, source_user_identifier:)
|
|
in_lock(
|
|
lock_key(source_user_identifier), ttl: LOCK_TTL, sleep_sec: LOCK_SLEEP, retries: LOCK_RETRIES
|
|
) do |retried|
|
|
if retried
|
|
source_user = find_source_user(source_user_identifier)
|
|
next source_user if source_user
|
|
end
|
|
|
|
create_source_user_mapping(source_name, source_username, source_user_identifier)
|
|
rescue ActiveRecord::RecordInvalid => e
|
|
raise unless e.record.errors.where(:source_user_identifier, :taken).any? # rubocop: disable CodeReuse/ActiveRecord -- not ActiveRecord
|
|
|
|
find_source_user(source_user_identifier)
|
|
end
|
|
end
|
|
|
|
def create_source_user_mapping(source_name, source_username, source_user_identifier)
|
|
::Import::SourceUser.transaction do
|
|
import_source_user = ::Import::SourceUser.new(
|
|
namespace: namespace,
|
|
import_type: import_type,
|
|
source_username: source_username,
|
|
source_name: source_name,
|
|
source_user_identifier: source_user_identifier,
|
|
source_hostname: source_hostname
|
|
)
|
|
|
|
import_source_user.placeholder_user = create_placeholder_user(import_source_user)
|
|
import_source_user.save!
|
|
import_source_user
|
|
end
|
|
rescue PG::UniqueViolation, ActiveRecord::RecordNotUnique => e
|
|
raise DuplicatedUserError.new(e.message), cause: e
|
|
rescue ActiveRecord::RecordInvalid => e
|
|
raise DuplicatedUserError.new(e.message), cause: e if duplicate_user_errors?(e.record)
|
|
|
|
raise
|
|
end
|
|
|
|
def create_placeholder_user(import_source_user)
|
|
return namespace_import_user if namespace.user_namespace? || placeholder_user_limit_exceeded?
|
|
|
|
Gitlab::Import::PlaceholderUserCreator.new(import_source_user).execute
|
|
end
|
|
|
|
def namespace_import_user
|
|
Gitlab::Import::ImportUserCreator.new(portable: namespace).execute
|
|
end
|
|
|
|
def placeholder_user_limit_exceeded?
|
|
::Import::PlaceholderUserLimit.new(namespace: namespace).exceeded?
|
|
end
|
|
|
|
def reset_source_user?(source_user)
|
|
source_user && source_user.mapped_user_id.nil?
|
|
end
|
|
|
|
def reset_source_user!(source_user)
|
|
in_lock(
|
|
lock_key(source_user.source_user_identifier), ttl: LOCK_TTL, sleep_sec: LOCK_SLEEP, retries: LOCK_RETRIES
|
|
) do |retried|
|
|
if retried
|
|
source_user.reset
|
|
next source_user unless reset_source_user?(source_user)
|
|
end
|
|
|
|
::Import::Framework::Logger.info(
|
|
message: 'Resetting source user state',
|
|
source_user_id: source_user.id,
|
|
source_user_status: source_user.status,
|
|
source_user_reassign_to_user_id: source_user.reassign_to_user_id,
|
|
source_user_placeholder_user_id: source_user.placeholder_user_id
|
|
)
|
|
|
|
source_user.status = 0
|
|
source_user.reassignment_token = nil
|
|
source_user.reassign_to_user = nil
|
|
source_user.placeholder_user ||= create_placeholder_user(source_user)
|
|
|
|
next source_user if source_user.save
|
|
|
|
::Import::Framework::Logger.error(
|
|
message: 'Failed to save source user after resetting',
|
|
source_user_id: source_user.id,
|
|
source_user_validation_errors: source_user.errors.full_messages
|
|
)
|
|
|
|
source_user.destroy
|
|
nil
|
|
end
|
|
end
|
|
|
|
def lock_key(source_user_identifier)
|
|
"import:source_user_mapper:#{namespace.id}:#{import_type}:#{source_hostname}:#{source_user_identifier}"
|
|
end
|
|
|
|
# Check validation errors on User records (placeholder or import users) for non-uniqueness.
|
|
# rubocop: disable CodeReuse/ActiveRecord -- not ActiveRecord
|
|
def duplicate_user_errors?(record)
|
|
record.errors.where(:email, :taken).any? || record.errors.where(:username, :taken).any?
|
|
end
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
|
end
|
|
end
|
|
end
|