Welcome to mirror list, hosted at ThFree Co, Russian Federation.

changes.rb « project_authorizations « models « app - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 1f0cec1a50cb07d2201700e9c2052bb403821ea7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# 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|
        attributes_batch.each { |attrs| attrs[:is_unique] = true }

        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