Commit 37cdb0f5 authored by Adam Hegyi's avatar Adam Hegyi

Optimize issue rebalancing UPDATE query

This change updates the issue rebalancing UPDATE query by sorting the
updatable issues by id. This will improve the data locality a bit and
probably speed up random I/O.
parent bea68b43
......@@ -17,8 +17,14 @@ class IssueRebalancingService
start = RelativePositioning::START_POSITION - (gaps / 2) * gap_size
Issue.transaction do
indexed_ids.each_slice(100) { |pairs| assign_positions(start, pairs) }
if Feature.enabled?(:issue_rebalancing_optimization)
Issue.transaction do
sort_pairs_by_id(start).each_slice(100) { |pairs| assign_positions_without_position_calculation(pairs) }
end
else
Issue.transaction do
indexed_ids.each_slice(100) { |pairs| assign_positions(start, pairs) }
end
end
end
......@@ -32,13 +38,30 @@ class IssueRebalancingService
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def sort_pairs_by_id(start)
indexed_ids.map do |id, index|
[id, start + (index * gap_size)]
end.sort_by(&:first)
end
def assign_positions_without_position_calculation(pairs_with_position)
values = pairs_with_position.map do |id, index|
"(#{id}, #{index})"
end.join(', ')
run_update_query(values)
end
def assign_positions(start, positions)
values = positions.map do |id, index|
"(#{id}, #{start + (index * gap_size)})"
end.join(', ')
Issue.connection.exec_query(<<~SQL, "rebalance issue positions")
run_update_query(values)
end
def run_update_query(values)
Issue.connection.exec_query(<<~SQL, "rebalance issue positions batches ordered by id")
WITH cte(cte_id, new_pos) AS (
SELECT *
FROM (VALUES #{values}) as t (id, pos)
......@@ -49,7 +72,6 @@ class IssueRebalancingService
WHERE cte_id = id
SQL
end
# rubocop: enable CodeReuse/ActiveRecord
def issue_count
@issue_count ||= base.count
......
---
name: issue_rebalancing_optimization
introduced_by_url:
rollout_issue_url:
milestone: '13.9'
type: development
group: group::plan
default_enabled: false
......@@ -32,70 +32,88 @@ RSpec.describe IssueRebalancingService do
project.reload.issues.reorder(relative_position: :asc).to_a
end
it 'rebalances a set of issues with clumps at the end and start' do
all_issues = start_clump + unclumped + end_clump.reverse
service = described_class.new(project.issues.first)
shared_examples 'IssueRebalancingService shared examples' do
it 'rebalances a set of issues with clumps at the end and start' do
all_issues = start_clump + unclumped + end_clump.reverse
service = described_class.new(project.issues.first)
expect { service.execute }.not_to change { issues_in_position_order.map(&:id) }
expect { service.execute }.not_to change { issues_in_position_order.map(&:id) }
all_issues.each(&:reset)
all_issues.each(&:reset)
gaps = all_issues.take(all_issues.count - 1).zip(all_issues.drop(1)).map do |a, b|
b.relative_position - a.relative_position
gaps = all_issues.take(all_issues.count - 1).zip(all_issues.drop(1)).map do |a, b|
b.relative_position - a.relative_position
end
expect(gaps).to all(be > RelativePositioning::MIN_GAP)
expect(all_issues.first.relative_position).to be > (RelativePositioning::MIN_POSITION * 0.9999)
expect(all_issues.last.relative_position).to be < (RelativePositioning::MAX_POSITION * 0.9999)
end
expect(gaps).to all(be > RelativePositioning::MIN_GAP)
expect(all_issues.first.relative_position).to be > (RelativePositioning::MIN_POSITION * 0.9999)
expect(all_issues.last.relative_position).to be < (RelativePositioning::MAX_POSITION * 0.9999)
end
it 'is idempotent' do
service = described_class.new(project.issues.first)
it 'is idempotent' do
service = described_class.new(project.issues.first)
expect do
service.execute
service.execute
end.not_to change { issues_in_position_order.map(&:id) }
end
expect do
service.execute
service.execute
end.not_to change { issues_in_position_order.map(&:id) }
end
it 'does nothing if the feature flag is disabled' do
stub_feature_flags(rebalance_issues: false)
issue = project.issues.first
issue.project
issue.project.group
old_pos = issue.relative_position
it 'does nothing if the feature flag is disabled' do
stub_feature_flags(rebalance_issues: false)
issue = project.issues.first
issue.project
issue.project.group
old_pos = issue.relative_position
service = described_class.new(issue)
service = described_class.new(issue)
expect { service.execute }.not_to exceed_query_limit(0)
expect(old_pos).to eq(issue.reload.relative_position)
end
expect { service.execute }.not_to exceed_query_limit(0)
expect(old_pos).to eq(issue.reload.relative_position)
end
it 'acts if the flag is enabled for the project' do
issue = create(:issue, project: project, author: user, relative_position: max_pos)
stub_feature_flags(rebalance_issues: issue.project)
it 'acts if the flag is enabled for the project' do
issue = create(:issue, project: project, author: user, relative_position: max_pos)
stub_feature_flags(rebalance_issues: issue.project)
service = described_class.new(issue)
service = described_class.new(issue)
expect { service.execute }.to change { issue.reload.relative_position }
end
expect { service.execute }.to change { issue.reload.relative_position }
end
it 'acts if the flag is enabled for the group' do
issue = create(:issue, project: project, author: user, relative_position: max_pos)
project.update!(group: create(:group))
stub_feature_flags(rebalance_issues: issue.project.group)
it 'acts if the flag is enabled for the group' do
issue = create(:issue, project: project, author: user, relative_position: max_pos)
project.update!(group: create(:group))
stub_feature_flags(rebalance_issues: issue.project.group)
service = described_class.new(issue)
service = described_class.new(issue)
expect { service.execute }.to change { issue.reload.relative_position }
end
it 'aborts if there are too many issues' do
issue = project.issues.first
base = double(count: 10_001)
expect { service.execute }.to change { issue.reload.relative_position }
allow(Issue).to receive(:relative_positioning_query_base).with(issue).and_return(base)
expect { described_class.new(issue).execute }.to raise_error(described_class::TooManyIssues)
end
end
it 'aborts if there are too many issues' do
issue = project.issues.first
base = double(count: 10_001)
context 'when issue_rebalancing_optimization feature flag is on' do
before do
stub_feature_flags(issue_rebalancing_optimization: true)
end
it_behaves_like 'IssueRebalancingService shared examples'
end
allow(Issue).to receive(:relative_positioning_query_base).with(issue).and_return(base)
context 'when issue_rebalancing_optimization feature flag is on' do
before do
stub_feature_flags(issue_rebalancing_optimization: false)
end
expect { described_class.new(issue).execute }.to raise_error(described_class::TooManyIssues)
it_behaves_like 'IssueRebalancingService shared examples'
end
end
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