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 ...@@ -17,8 +17,14 @@ class IssueRebalancingService
start = RelativePositioning::START_POSITION - (gaps / 2) * gap_size start = RelativePositioning::START_POSITION - (gaps / 2) * gap_size
Issue.transaction do if Feature.enabled?(:issue_rebalancing_optimization)
indexed_ids.each_slice(100) { |pairs| assign_positions(start, pairs) } 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
end end
...@@ -32,13 +38,30 @@ class IssueRebalancingService ...@@ -32,13 +38,30 @@ class IssueRebalancingService
end end
# rubocop: enable CodeReuse/ActiveRecord # 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) def assign_positions(start, positions)
values = positions.map do |id, index| values = positions.map do |id, index|
"(#{id}, #{start + (index * gap_size)})" "(#{id}, #{start + (index * gap_size)})"
end.join(', ') 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 ( WITH cte(cte_id, new_pos) AS (
SELECT * SELECT *
FROM (VALUES #{values}) as t (id, pos) FROM (VALUES #{values}) as t (id, pos)
...@@ -49,7 +72,6 @@ class IssueRebalancingService ...@@ -49,7 +72,6 @@ class IssueRebalancingService
WHERE cte_id = id WHERE cte_id = id
SQL SQL
end end
# rubocop: enable CodeReuse/ActiveRecord
def issue_count def issue_count
@issue_count ||= base.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 ...@@ -32,70 +32,88 @@ RSpec.describe IssueRebalancingService do
project.reload.issues.reorder(relative_position: :asc).to_a project.reload.issues.reorder(relative_position: :asc).to_a
end end
it 'rebalances a set of issues with clumps at the end and start' do shared_examples 'IssueRebalancingService shared examples' do
all_issues = start_clump + unclumped + end_clump.reverse it 'rebalances a set of issues with clumps at the end and start' do
service = described_class.new(project.issues.first) 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| gaps = all_issues.take(all_issues.count - 1).zip(all_issues.drop(1)).map do |a, b|
b.relative_position - a.relative_position 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 end
expect(gaps).to all(be > RelativePositioning::MIN_GAP) it 'is idempotent' do
expect(all_issues.first.relative_position).to be > (RelativePositioning::MIN_POSITION * 0.9999) service = described_class.new(project.issues.first)
expect(all_issues.last.relative_position).to be < (RelativePositioning::MAX_POSITION * 0.9999)
end
it 'is idempotent' do expect do
service = described_class.new(project.issues.first) service.execute
service.execute
end.not_to change { issues_in_position_order.map(&:id) }
end
expect do it 'does nothing if the feature flag is disabled' do
service.execute stub_feature_flags(rebalance_issues: false)
service.execute issue = project.issues.first
end.not_to change { issues_in_position_order.map(&:id) } issue.project
end issue.project.group
old_pos = issue.relative_position
it 'does nothing if the feature flag is disabled' do service = described_class.new(issue)
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) 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) it 'acts if the flag is enabled for the project' do
expect(old_pos).to eq(issue.reload.relative_position) issue = create(:issue, project: project, author: user, relative_position: max_pos)
end stub_feature_flags(rebalance_issues: issue.project)
it 'acts if the flag is enabled for the project' do service = described_class.new(issue)
issue = create(:issue, project: project, author: user, relative_position: max_pos)
stub_feature_flags(rebalance_issues: issue.project)
service = described_class.new(issue) expect { service.execute }.to change { issue.reload.relative_position }
end
expect { service.execute }.to change { issue.reload.relative_position } it 'acts if the flag is enabled for the group' do
end 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 service = described_class.new(issue)
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) 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 end
it 'aborts if there are too many issues' do context 'when issue_rebalancing_optimization feature flag is on' do
issue = project.issues.first before do
base = double(count: 10_001) 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
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