Commit cc66f7b7 authored by Jarka Košanová's avatar Jarka Košanová

Move migration to the background

- and code & specs improvements
parent 18b1cae4
...@@ -3,35 +3,29 @@ ...@@ -3,35 +3,29 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
class MoveEpicIssuesAfterEpics < ActiveRecord::Migration[5.2] class ScheduleEpicIssuesAfterEpicsMove < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
DOWNTIME = false DOWNTIME = false
BATCH_SIZE = 1_000 INTERVAL = 5.minutes.to_i
BATCH_SIZE = 100
MIGRATION = 'MoveEpicIssuesAfterEpics'
disable_ddl_transaction! disable_ddl_transaction!
class Epic < ActiveRecord::Base class Epic < ActiveRecord::Base
self.table_name = 'epics' self.table_name = 'epics'
end
class EpicIssue < ActiveRecord::Base
self.table_name = 'epic_issues'
include ::EachBatch include ::EachBatch
end end
def up def up
maximum_epic_position = Epic.maximum(:relative_position) return unless ::Gitlab.ee?
return unless maximum_epic_position
max_position = Gitlab::Database::MAX_INT_VALUE
delta = ((maximum_epic_position - max_position) / 2.0).abs.ceil
EpicIssue.where('relative_position < ?', maximum_epic_position).each_batch(of: BATCH_SIZE) do |batch, _| Epic.each_batch(of: BATCH_SIZE) do |batch, index|
batch.update_all("relative_position = relative_position + #{delta}") range = batch.pluck('MIN(id)', 'MAX(id)').first
delay = index * interval
BackgroundMigrationWorker.perform_in(delay, MIGRATION, *range)
end end
end end
......
...@@ -21,11 +21,13 @@ module EpicTreeSorting ...@@ -21,11 +21,13 @@ module EpicTreeSorting
included do included do
def move_sequence(start_pos, end_pos, delta) def move_sequence(start_pos, end_pos, delta)
items_to_update = scoped_items items_to_update = scoped_items
.select(:id, :object_type)
.where('relative_position BETWEEN ? AND ?', start_pos, end_pos) .where('relative_position BETWEEN ? AND ?', start_pos, end_pos)
.where.not('object_type = ? AND id = ?', self.class.table_name.singularize, self.id)
items_to_update.group_by { |item| item.object_type }.each do |type, group_items| items_to_update.group_by { |item| item.object_type }.each do |type, group_items|
items = type.camelcase.constantize.where(id: group_items.map(&:id)) ids = group_items.map(&:id)
items = items.where.not(id: self.id) if type == self.class.underscore items = type.camelcase.constantize.where(id: ids).select(:id)
items.update_all("relative_position = relative_position + #{delta}") items.update_all("relative_position = relative_position + #{delta}")
end end
end end
......
...@@ -27,24 +27,30 @@ module Epics ...@@ -27,24 +27,30 @@ module Epics
end end
def before_object def before_object
return unless params[:relative_position].to_sym == :before return unless params[:relative_position] == 'before'
adjacent_reference adjacent_reference
end end
def after_object def after_object
return unless params[:relative_position].to_sym == :after return unless params[:relative_position] == 'after'
adjacent_reference adjacent_reference
end end
def validate_objects def validate_objects
return 'Relative position is not valid.' unless valid_relative_position?
unless supported_type?(moving_object) && supported_type?(adjacent_reference) unless supported_type?(moving_object) && supported_type?(adjacent_reference)
return 'Only epics and epic_issues are supported.' return 'Only epics and epic_issues are supported.'
end end
return 'You don\'t have permissions to move the objects.' unless authorized? return 'You don\'t have permissions to move the objects.' unless authorized?
return 'Both object have to belong to same parent epic.' unless same_parent? return 'Both objects have to belong to the same parent epic.' unless same_parent?
end
def valid_relative_position?
%w(before after).include?(params[:relative_position])
end end
def same_parent? def same_parent?
......
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
class MoveEpicIssuesAfterEpics
class EpicIssue < ActiveRecord::Base
self.table_name = 'epic_issues'
end
class Epic < ActiveRecord::Base
self.table_name = 'epics'
end
def perform(start_id, stop_id)
maximum_epic_position = Epic.maximum(:relative_position)
return unless maximum_epic_position
max_position = Gitlab::Database::MAX_INT_VALUE
delta = ((maximum_epic_position - max_position) / 2.0).abs.ceil
EpicIssue.where(epic_id: start_id..stop_id).where('relative_position < ?', max_position - delta)
.update_all("relative_position = relative_position + #{delta}")
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::BackgroundMigration::MoveEpicIssuesAfterEpics, :migration, schema: 20190926180443 do
let(:users) { table(:users) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:issues) { table(:issues) }
let(:epics) { table(:epics) }
let(:epic_issues) { table(:epic_issues) }
subject { described_class.new }
describe '#perform' do
let(:epic_params) do
{
title: 'Epic',
title_html: 'Epic',
group_id: group.id,
author_id: user.id
}
end
let(:issue_params) do
{
title: 'Issue',
title_html: 'Issue',
project_id: project.id,
author_id: user.id
}
end
let(:user) { users.create(name: 'test', email: 'test@example.com', projects_limit: 5) }
let(:group) { namespaces.create(name: 'gitlab', path: 'gitlab-org') }
context 'when there are epic_issues present' do
let(:project) { projects.create(namespace_id: group.id, name: 'foo') }
let(:base_epic) { epics.create(epic_params.merge(iid: 3, relative_position: 500)) }
let(:issue_1) { issues.create(issue_params.merge(iid: 1)) }
let(:issue_2) { issues.create(issue_params.merge(iid: 2)) }
let(:issue_3) { issues.create(issue_params.merge(iid: 3)) }
let!(:epic_1) { epics.create(epic_params.merge(iid: 1, relative_position: 100)) }
let!(:epic_2) { epics.create(epic_params.merge(iid: 2, relative_position: 5000)) }
let!(:epic_issue_1) { epic_issues.create(issue_id: issue_1.id, epic_id: base_epic.id, relative_position: 400) }
let!(:epic_issue_2) { epic_issues.create(issue_id: issue_2.id, epic_id: base_epic.id, relative_position: 5010) }
let!(:epic_issue_3) { epic_issues.create(issue_id: issue_3.id, epic_id: base_epic.id, relative_position: Gitlab::Database::MAX_INT_VALUE - 10) }
before do
subject.perform(epics.first.id, epics.last.id)
end
it 'does not change relative_position of epics' do
expect(base_epic.relative_position).to eq(500)
expect(epic_1.relative_position).to eq(100)
expect(epic_2.relative_position).to eq(5000)
end
it 'moves epic_issues after epics' do
expect(epic_issue_1.reload.relative_position).to be > 5000
expect(epic_issue_2.reload.relative_position).to be > 5000
end
it 'keeps epic_issues order' do
expect(epic_issue_1.reload.relative_position).to be < epic_issue_2.reload.relative_position
end
it 'does not change the relative_position of epic_issue getting to the max value' do
expect(epic_issue_3.reload.relative_position).to eq(Gitlab::Database::MAX_INT_VALUE - 10)
end
end
context 'when there are no epics' do
it 'runs correctly' do
expect(subject.perform(1, 10)).to be_nil
end
end
context 'when there are no epic_issues' do
it 'runs correctly' do
epics.create(epic_params.merge(iid: 3, relative_position: 500))
expect(subject.perform(1, 10)).to be_zero
end
end
end
end
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe EpicTreeSorting do describe EpicTreeSorting do
let(:group) { create(:group) } let_it_be(:group) { create(:group) }
let(:base_epic) { create(:epic, group: group) } let_it_be(:base_epic) { create(:epic, group: group) }
let!(:epic_issue1) { create(:epic_issue, epic: base_epic, relative_position: 10) } let!(:epic_issue1) { create(:epic_issue, epic: base_epic, relative_position: 10) }
let!(:epic_issue2) { create(:epic_issue, epic: base_epic, relative_position: 500) } let!(:epic_issue2) { create(:epic_issue, epic: base_epic, relative_position: 500) }
let!(:epic_issue3) { create(:epic_issue, epic: base_epic, relative_position: 1002) } let!(:epic_issue3) { create(:epic_issue, epic: base_epic, relative_position: 1002) }
...@@ -23,6 +23,7 @@ describe EpicTreeSorting do ...@@ -23,6 +23,7 @@ describe EpicTreeSorting do
epic_issue2.move_after(epic2) epic_issue2.move_after(epic2)
expect(epic_issue2.relative_position).to be_between(epic2.reload.relative_position, epic3.reload.relative_position).exclusive expect(epic_issue2.relative_position).to be_between(epic2.reload.relative_position, epic3.reload.relative_position).exclusive
expect(epic_issue3.reload.relative_position).to be > epic3.reload.relative_position
end end
end end
...@@ -53,4 +54,41 @@ describe EpicTreeSorting do ...@@ -53,4 +54,41 @@ describe EpicTreeSorting do
expect(epic_issue3.relative_position).to be_between(epic1.reload.relative_position, epic_issue2.reload.relative_position).exclusive expect(epic_issue3.relative_position).to be_between(epic1.reload.relative_position, epic_issue2.reload.relative_position).exclusive
end end
end end
context '#move_sequence' do
let!(:epic_issue1) { create(:epic_issue, epic: base_epic, relative_position: 1000) }
let!(:epic_issue2) { create(:epic_issue, epic: base_epic, relative_position: 1001) }
let!(:epic_issue3) { create(:epic_issue, epic: base_epic, relative_position: 1004) }
let!(:epic1) { create(:epic, parent: base_epic, group: group, relative_position: 1002) }
let!(:epic2) { create(:epic, parent: base_epic, group: group, relative_position: 1003) }
let!(:epic3) { create(:epic, parent: base_epic, group: group, relative_position: 1005) }
context 'when self is an epic' do
it 'moves all objects correctly' do
epic1.move_sequence(1003, 1005, 500)
expect(epic_issue1.reload.relative_position).to eq(1000)
expect(epic_issue2.reload.relative_position).to eq(1001)
expect(epic_issue3.reload.relative_position).to eq(1504)
expect(epic1.reload.relative_position).to eq(1002)
expect(epic2.reload.relative_position).to eq(1503)
expect(epic3.reload.relative_position).to eq(1505)
end
end
context 'when self is an epic_issue' do
it 'moves all objects correctly' do
epic_issue1.move_sequence(1001, 1005, 500)
expect(epic_issue1.reload.relative_position).to eq(1000)
expect(epic_issue2.reload.relative_position).to eq(1501)
expect(epic_issue3.reload.relative_position).to eq(1504)
expect(epic1.reload.relative_position).to eq(1502)
expect(epic2.reload.relative_position).to eq(1503)
expect(epic3.reload.relative_position).to eq(1505)
end
end
end
end end
...@@ -106,7 +106,7 @@ describe 'Updating an epic tree' do ...@@ -106,7 +106,7 @@ describe 'Updating an epic tree' do
end end
end end
context 'when moving an epic fails due another parent' do context 'when moving an epic fails due to another parent' do
let(:epic2) { create(:epic, relative_position: 20) } let(:epic2) { create(:epic, relative_position: 20) }
it_behaves_like 'a mutation that does not update the tree' it_behaves_like 'a mutation that does not update the tree'
...@@ -114,7 +114,7 @@ describe 'Updating an epic tree' do ...@@ -114,7 +114,7 @@ describe 'Updating an epic tree' do
it 'returns the error message' do it 'returns the error message' do
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['errors']).to eq(['Both object have to belong to same parent epic.']) expect(mutation_response['errors']).to eq(['Both objects have to belong to the same parent epic.'])
end end
end end
end end
...@@ -151,7 +151,7 @@ describe 'Updating an epic tree' do ...@@ -151,7 +151,7 @@ describe 'Updating an epic tree' do
it 'returns the error message' do it 'returns the error message' do
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['errors']).to eq(['Both object have to belong to same parent epic.']) expect(mutation_response['errors']).to eq(['Both objects have to belong to the same parent epic.'])
end end
end end
end end
......
...@@ -15,7 +15,7 @@ describe Epics::TreeReorderService do ...@@ -15,7 +15,7 @@ describe Epics::TreeReorderService do
let(:epic_issue1) { create(:epic_issue, epic: epic, issue: issue1, relative_position: 30) } let(:epic_issue1) { create(:epic_issue, epic: epic, issue: issue1, relative_position: 30) }
let(:epic_issue2) { create(:epic_issue, epic: epic, issue: issue2, relative_position: 40) } let(:epic_issue2) { create(:epic_issue, epic: epic, issue: issue2, relative_position: 40) }
let(:relative_position) { :after } let(:relative_position) { 'after' }
let!(:tree_object_1) { epic1 } let!(:tree_object_1) { epic1 }
let!(:tree_object_2) { epic2 } let!(:tree_object_2) { epic2 }
let(:adjacent_reference_id) { GitlabSchema.id_from_object(tree_object_1) } let(:adjacent_reference_id) { GitlabSchema.id_from_object(tree_object_1) }
...@@ -45,7 +45,7 @@ describe Epics::TreeReorderService do ...@@ -45,7 +45,7 @@ describe Epics::TreeReorderService do
end end
end end
context 'when epics feature is enabled' do context 'when epics feature is not enabled' do
it_behaves_like 'error for the tree update', 'You don\'t have permissions to move the objects.' it_behaves_like 'error for the tree update', 'You don\'t have permissions to move the objects.'
end end
...@@ -63,6 +63,12 @@ describe Epics::TreeReorderService do ...@@ -63,6 +63,12 @@ describe Epics::TreeReorderService do
group.add_developer(user) group.add_developer(user)
end end
context 'when relative_position is not valid' do
let(:relative_position) { 'whatever' }
it_behaves_like 'error for the tree update', 'Relative position is not valid.'
end
context 'when moving EpicIssue' do context 'when moving EpicIssue' do
let!(:tree_object_1) { epic_issue1 } let!(:tree_object_1) { epic_issue1 }
let!(:tree_object_2) { epic_issue2 } let!(:tree_object_2) { epic_issue2 }
...@@ -93,7 +99,7 @@ describe Epics::TreeReorderService do ...@@ -93,7 +99,7 @@ describe Epics::TreeReorderService do
epic_issue2.update(epic: other_epic) epic_issue2.update(epic: other_epic)
end end
it_behaves_like 'error for the tree update', 'Both object have to belong to same parent epic.' it_behaves_like 'error for the tree update', 'Both objects have to belong to the same parent epic.'
end end
context 'when object being moved is not supported type' do context 'when object being moved is not supported type' do
...@@ -145,13 +151,13 @@ describe Epics::TreeReorderService do ...@@ -145,13 +151,13 @@ describe Epics::TreeReorderService do
it_behaves_like 'error for the tree update', 'You don\'t have permissions to move the objects.' it_behaves_like 'error for the tree update', 'You don\'t have permissions to move the objects.'
end end
context 'when object being moved is from of another epic' do context 'when object being moved is froms another epic' do
before do before do
other_epic = create(:epic, group: group) other_epic = create(:epic, group: group)
epic2.update(parent: other_epic) epic2.update(parent: other_epic)
end end
it_behaves_like 'error for the tree update', 'Both object have to belong to same parent epic.' it_behaves_like 'error for the tree update', 'Both objects have to belong to the same parent epic.'
end end
context 'when moving is successful' do context 'when moving is successful' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment