2018-01-02 11:21:28 -05:00
|
|
|
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
|
|
|
|
# for more information on how to write migrations for GitLab.
|
|
|
|
|
2018-11-13 02:27:31 -05:00
|
|
|
class RemoveSoftRemovedObjects < ActiveRecord::Migration[4.2]
|
2018-01-02 11:21:28 -05:00
|
|
|
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
|
2018-08-10 19:45:46 -04:00
|
|
|
disable_statement_timeout do
|
2018-07-09 17:31:45 -04:00
|
|
|
remove_personal_routes
|
|
|
|
remove_personal_namespaces
|
|
|
|
remove_group_namespaces
|
|
|
|
remove_simple_soft_removed_rows
|
|
|
|
end
|
2018-01-02 11:21:28 -05:00
|
|
|
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
|
2018-01-10 09:48:53 -05:00
|
|
|
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
|
|
|
|
|
2018-01-02 11:21:28 -05:00
|
|
|
# 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
|
2018-01-10 09:48:53 -05:00
|
|
|
User.where(admin: true).limit(1).pluck(:id).first
|
2018-01-02 11:21:28 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def migrate_inline?
|
|
|
|
Rails.env.test? || Rails.env.development?
|
|
|
|
end
|
|
|
|
end
|