# See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. class RemoveSoftRemovedObjects < ActiveRecord::Migration[4.2] include Gitlab::Database::MigrationHelpers # Set this constant to true if this migration requires downtime. DOWNTIME = false disable_ddl_transaction! module SoftRemoved extend ActiveSupport::Concern included do scope :soft_removed, -> { where('deleted_at IS NOT NULL') } end end class User < ActiveRecord::Base self.table_name = 'users' include EachBatch end class Issue < ActiveRecord::Base self.table_name = 'issues' include EachBatch include SoftRemoved end class MergeRequest < ActiveRecord::Base self.table_name = 'merge_requests' include EachBatch include SoftRemoved end class Namespace < ActiveRecord::Base self.table_name = 'namespaces' include EachBatch include SoftRemoved scope :soft_removed_personal, -> { soft_removed.where(type: nil) } scope :soft_removed_group, -> { soft_removed.where(type: 'Group') } end class Route < ActiveRecord::Base self.table_name = 'routes' include EachBatch include SoftRemoved end class Project < ActiveRecord::Base self.table_name = 'projects' include EachBatch include SoftRemoved end class CiPipelineSchedule < ActiveRecord::Base self.table_name = 'ci_pipeline_schedules' include EachBatch include SoftRemoved end class CiTrigger < ActiveRecord::Base self.table_name = 'ci_triggers' include EachBatch include SoftRemoved end MODELS = [Issue, MergeRequest, CiPipelineSchedule, CiTrigger].freeze def up disable_statement_timeout do remove_personal_routes remove_personal_namespaces remove_group_namespaces remove_simple_soft_removed_rows end end def down # The data removed by this migration can't be restored in an automated way. end def remove_simple_soft_removed_rows create_temporary_indexes MODELS.each do |model| say_with_time("Removing soft removed rows from #{model.table_name}") do model.soft_removed.each_batch do |batch, index| batch.delete_all end end end ensure remove_temporary_indexes end def create_temporary_indexes MODELS.each do |model| index_name = temporary_index_name_for(model) # Without this index the removal process can take a very long time. For # example, getting the next ID of a batch for the `issues` table in # staging would take between 15 and 20 seconds. next if temporary_index_exists?(model) say_with_time("Creating temporary index #{index_name}") do add_concurrent_index( model.table_name, [:deleted_at, :id], name: index_name, where: 'deleted_at IS NOT NULL' ) end end end def remove_temporary_indexes MODELS.each do |model| index_name = temporary_index_name_for(model) next unless temporary_index_exists?(model) say_with_time("Removing temporary index #{index_name}") do remove_concurrent_index_by_name(model.table_name, index_name) end end end def temporary_index_name_for(model) "index_on_#{model.table_name}_tmp" end def temporary_index_exists?(model) index_name = temporary_index_name_for(model) index_exists?(model.table_name, [:deleted_at, :id], name: index_name) end def remove_personal_namespaces # Some personal namespaces are left behind in case of GitLab.com. In these # cases the associated data such as the projects and users has already been # removed. Namespace.soft_removed_personal.each_batch do |batch| batch.delete_all end end def remove_group_namespaces admin_id = id_for_admin_user unless admin_id say 'Not scheduling soft removed groups for removal as no admin user ' \ 'could be found. You will need to remove any such groups manually.' return end # Left over groups can't be easily removed because we may also need to # remove memberships, repositories, and other associated data. As a result # we'll just schedule a Sidekiq job to remove these. # # As of January 5th, 2018 there are 36 groups that will be removed using # this code. Namespace.select(:id).soft_removed_group.each_batch(of: 10) do |batch, index| batch.each do |ns| schedule_group_removal(index * 5.minutes, ns.id, admin_id) end end end def schedule_group_removal(delay, group_id, user_id) if migrate_inline? GroupDestroyWorker.new.perform(group_id, user_id) else GroupDestroyWorker.perform_in(delay, group_id, user_id) end end def remove_personal_routes namespaces = Namespace.select(1) .soft_removed .where('namespaces.type IS NULL') .where('routes.source_type = ?', 'Namespace') .where('routes.source_id = namespaces.id') Route.where('EXISTS (?)', namespaces).each_batch do |batch| batch.delete_all end end def id_for_admin_user User.where(admin: true).limit(1).pluck(:id).first end def migrate_inline? Rails.env.test? || Rails.env.development? end end