diff options
Diffstat (limited to 'app/models/project_authorizations/changes.rb')
-rw-r--r-- | app/models/project_authorizations/changes.rb | 143 |
1 files changed, 143 insertions, 0 deletions
diff --git a/app/models/project_authorizations/changes.rb b/app/models/project_authorizations/changes.rb new file mode 100644 index 00000000000..1d717950c1c --- /dev/null +++ b/app/models/project_authorizations/changes.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +module ProjectAuthorizations + # How to use this class + # authorizations_to_add: + # Rows to insert in the form `[{ user_id: user_id, project_id: project_id, access_level: access_level}, ...] + # + # ProjectAuthorizations::Changes.new do |changes| + # changes.add(authorizations_to_add) + # changes.remove_users_in_project(project, user_ids) + # changes.remove_projects_for_user(user, project_ids) + # end.apply! + class Changes + attr_reader :projects_to_remove, :users_to_remove, :authorizations_to_add + + BATCH_SIZE = 1000 + SLEEP_DELAY = 0.1 + + def initialize + @authorizations_to_add = [] + @affected_project_ids = Set.new + yield self + end + + def add(authorizations_to_add) + @authorizations_to_add += authorizations_to_add + end + + def remove_users_in_project(project, user_ids) + @users_to_remove = { user_ids: user_ids, scope: project } + end + + def remove_projects_for_user(user, project_ids) + @projects_to_remove = { project_ids: project_ids, scope: user } + end + + def apply! + delete_authorizations_for_user if should_delete_authorizations_for_user? + delete_authorizations_for_project if should_delete_authorizations_for_project? + add_authorizations if should_add_authorization? + + publish_events + end + + private + + def should_add_authorization? + authorizations_to_add.present? + end + + def should_delete_authorizations_for_user? + user && project_ids.present? + end + + def should_delete_authorizations_for_project? + project && user_ids.present? + end + + def add_authorizations + insert_all_in_batches(authorizations_to_add) + @affected_project_ids += authorizations_to_add.pluck(:project_id) + end + + def delete_authorizations_for_user + delete_all_in_batches(resource: user, + ids_to_remove: project_ids, + column_name_of_ids_to_remove: :project_id) + @affected_project_ids += project_ids + end + + def delete_authorizations_for_project + delete_all_in_batches(resource: project, + ids_to_remove: user_ids, + column_name_of_ids_to_remove: :user_id) + @affected_project_ids << project.id + end + + def delete_all_in_batches(resource:, ids_to_remove:, column_name_of_ids_to_remove:) + add_delay = add_delay_between_batches?(entire_size: ids_to_remove.size, batch_size: BATCH_SIZE) + log_details(entire_size: ids_to_remove.size, batch_size: BATCH_SIZE) if add_delay + + ids_to_remove.each_slice(BATCH_SIZE) do |ids_batch| + resource.project_authorizations.where(column_name_of_ids_to_remove => ids_batch).delete_all + perform_delay if add_delay + end + end + + def insert_all_in_batches(attributes) + add_delay = add_delay_between_batches?(entire_size: attributes.size, batch_size: BATCH_SIZE) + log_details(entire_size: attributes.size, batch_size: BATCH_SIZE) if add_delay + + attributes.each_slice(BATCH_SIZE) do |attributes_batch| + ProjectAuthorization.insert_all(attributes_batch) + perform_delay if add_delay + end + end + + def add_delay_between_batches?(entire_size:, batch_size:) + # The reason for adding a delay is to give the replica database enough time to + # catch up with the primary when large batches of records are being added/removed. + # Hence, we add a delay only if the GitLab installation has a replica database configured. + entire_size > batch_size && + !::Gitlab::Database::LoadBalancing.primary_only? + end + + def log_details(entire_size:, batch_size:) + Gitlab::AppLogger.info( + entire_size: entire_size, + total_delay: (entire_size / batch_size.to_f).ceil * SLEEP_DELAY, + message: 'Project authorizations refresh performed with delay', + **Gitlab::ApplicationContext.current + ) + end + + def perform_delay + sleep(SLEEP_DELAY) + end + + def user + projects_to_remove&.[](:scope) + end + + def project_ids + projects_to_remove&.[](:project_ids) + end + + def project + users_to_remove&.[](:scope) + end + + def user_ids + users_to_remove&.[](:user_ids) + end + + def publish_events + @affected_project_ids.each do |project_id| + ::Gitlab::EventStore.publish( + ::ProjectAuthorizations::AuthorizationsChangedEvent.new(data: { project_id: project_id }) + ) + end + end + end +end |