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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'db/post_migrate/20201106134950_deduplicate_epic_iids.rb')
-rw-r--r--db/post_migrate/20201106134950_deduplicate_epic_iids.rb121
1 files changed, 121 insertions, 0 deletions
diff --git a/db/post_migrate/20201106134950_deduplicate_epic_iids.rb b/db/post_migrate/20201106134950_deduplicate_epic_iids.rb
new file mode 100644
index 00000000000..bc7daf9329d
--- /dev/null
+++ b/db/post_migrate/20201106134950_deduplicate_epic_iids.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+class DeduplicateEpicIids < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_epics_on_group_id_and_iid'
+
+ disable_ddl_transaction!
+
+ class Epic < ActiveRecord::Base
+ end
+
+ class InternalId < ActiveRecord::Base
+ class << self
+ def generate_next(subject, scope, usage, init)
+ InternalIdGenerator.new(subject, scope, usage, init).generate
+ end
+ end
+
+ # Increments #last_value and saves the record
+ #
+ # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
+ # As such, the increment is atomic and safe to be called concurrently.
+ def increment_and_save!
+ update_and_save { self.last_value = (last_value || 0) + 1 }
+ end
+
+ private
+
+ def update_and_save(&block)
+ lock!
+ yield
+ save!
+ last_value
+ end
+ end
+
+ # See app/models/internal_id
+ class InternalIdGenerator
+ attr_reader :subject, :scope, :scope_attrs, :usage, :init
+
+ def initialize(subject, scope, usage, init = nil)
+ @subject = subject
+ @scope = scope
+ @usage = usage
+ @init = init
+
+ raise ArgumentError, 'Scope is not well-defined, need at least one column for scope (given: 0)' if scope.empty? || usage.to_s != 'epics'
+ end
+
+ # Generates next internal id and returns it
+ # init: Block that gets called to initialize InternalId record if not present
+ # Make sure to not throw exceptions in the absence of records (if this is expected).
+ def generate
+ subject.transaction do
+ # Create a record in internal_ids if one does not yet exist
+ # and increment its last value
+ #
+ # Note this will acquire a ROW SHARE lock on the InternalId record
+ record.increment_and_save!
+ end
+ end
+
+ def record
+ @record ||= (lookup || create_record)
+ end
+
+ def lookup
+ InternalId.find_by(**scope, usage: usage_value)
+ end
+
+ def usage_value
+ 4 # see Enums::InternalId - this is the value for epics
+ end
+
+ # Create InternalId record for (scope, usage) combination, if it doesn't exist
+ #
+ # We blindly insert without synchronization. If another process
+ # was faster in doing this, we'll realize once we hit the unique key constraint
+ # violation. We can safely roll-back the nested transaction and perform
+ # a lookup instead to retrieve the record.
+ def create_record
+ raise ArgumentError, 'Cannot initialize without init!' unless init
+
+ instance = subject.is_a?(::Class) ? nil : subject
+
+ subject.transaction(requires_new: true) do
+ InternalId.create!(
+ **scope,
+ usage: usage_value,
+ last_value: init.call(instance, scope) || 0
+ )
+ end
+ rescue ActiveRecord::RecordNotUnique
+ lookup
+ end
+ end
+
+ def up
+ duplicate_epic_ids = ApplicationRecord.connection.execute('SELECT iid, group_id, COUNT(*) FROM epics GROUP BY iid, group_id HAVING COUNT(*) > 1;')
+
+ duplicate_epic_ids.each do |dup|
+ Epic.where(iid: dup['iid'], group_id: dup['group_id']).last(dup['count'] - 1).each do |epic|
+ new_iid = InternalId.generate_next(epic,
+ { namespace_id: epic.group_id },
+ :epics, ->(instance, _) { instance.class.where(group_id: epic.group_id).maximum(:iid) }
+ )
+
+ epic.update!(iid: new_iid)
+ end
+ end
+
+ add_concurrent_index :epics, [:group_id, :iid], unique: true, name: INDEX_NAME
+ end
+
+ def down
+ # only remove the index, as we do not want to create the duplicates back
+ remove_concurrent_index :epics, [:group_id, :iid], name: INDEX_NAME
+ end
+end