diff options
Diffstat (limited to 'db/post_migrate/20201106134950_deduplicate_epic_iids.rb')
-rw-r--r-- | db/post_migrate/20201106134950_deduplicate_epic_iids.rb | 121 |
1 files changed, 0 insertions, 121 deletions
diff --git a/db/post_migrate/20201106134950_deduplicate_epic_iids.rb b/db/post_migrate/20201106134950_deduplicate_epic_iids.rb deleted file mode 100644 index 8fddc81057b..00000000000 --- a/db/post_migrate/20201106134950_deduplicate_epic_iids.rb +++ /dev/null @@ -1,121 +0,0 @@ -# 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 # rubocop:disable Performance/ActiveRecordSubtransactions - 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 |