Commit 4982354c authored by Alex Kalderimis's avatar Alex Kalderimis

Add tests for rebalancing service

parent 8c413af4
# frozen_string_literal: true # frozen_string_literal: true
module Issue class IssueRebalancingService < Issues::BaseService
class RebalancingService < Issues::BaseService MAX_ISSUE_COUNT = 100_000
MAX_ISSUE_COUNT = 100_000 TooManyIssues = Class.new(StandardError)
TooManyIssues = Class.new(StandardError)
attr_reader :issue attr_reader :issue
def initialize(issue) def initialize(issue)
@issue = issue @issue = issue
end
# rubocop: disable CodeReuse/ActiveRecord
def execute
base = Issue.relative_positioning_query_base(issue)
n = base.count
if n > MAX_ISSUE_COUNT
raise TooManyIssues, "#{n} issues"
end
gaps = n - 1
gap_size = 0
ratio = 0.5
while gap_size < RelativePositioning::MIN_GAP && ratio < 1
gap_size = (ratio * Gitlab::Database::MAX_INT_VALUE) / (gaps / 2)
ratio += 0.1
end
# If there are 4 billion issues, then we cannot rebalance them
if gap_size < RelativePositioning::MIN_GAP
raise RelativePositioning::NoSpaceLeft
end end
# rubocop: disable CodeReuse/ActiveRecord start = 0 - (gaps / 2) * gap_size
def execute
base = Issue.relative_positioning_query_base(issue) indexed = base.reorder(:relative_position, :id).pluck(:id).each_with_index
n = base.count indexed.each_slice(500) do |pairs|
values = pairs.map do |id, index|
if n > MAX_ISSUE_COUNT "(#{id}, #{start + (index * gap_size)})"
raise TooManyIssues, "#{n} issues" end.join(', ')
end
Issue.connection.exec_query(<<~SQL, "rebalance issue positions")
gaps = n - 1 WITH cte(cte_id, new_pos) AS (
gap_size = 0 SELECT *
ratio = 0.5 FROM (VALUES #{values}) as t (id, pos)
)
while gap_size < RelativePositioning::MIN_GAP && ratio < 1 UPDATE #{Issue.table_name}
gap_size = (ratio * Gitlab::Database::MAX_INT_VALUE) / (gaps / 2) SET relative_position = cte.new_pos
ratio += 0.1 FROM cte
end WHERE cte_id = id
SQL
# If there are 4 billion issues, then we cannot rebalance them
if gap_size < RelativePositioning::MIN_GAP
raise RelativePositioning::NoSpaceLeft
end
start = 0 - (gaps / 2) * gap_size
indexed = base.reorder(:relative_position, :id).pluck(:id).each_with_index
indexed.each_slice(500) do |pairs|
values = pairs.map do |id, index|
"(#{id}, #{start + (index * gap_size)})"
end.join(', ')
Issue.connection.exec_query(<<~SQL, "rebalance issue positions")
WITH cte(cte_id, new_pos) AS (
SELECT *
FROM (VALUES #{values}) as t (id, pos)
)
UPDATE #{Issue.table_name}
SET relative_position = cte.new_pos
FROM cte
WHERE cte_id = id
SQL
end
end end
# rubocop: enable CodeReuse/ActiveRecord
end end
# rubocop: enable CodeReuse/ActiveRecord
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IssueRebalancingService do
let_it_be(:project) { create(:project) }
let_it_be(:user) { project.creator }
let_it_be(:start) { RelativePositioning::START_POSITION }
let_it_be(:max_pos) { RelativePositioning::MAX_POSITION }
let_it_be(:min_pos) { RelativePositioning::MIN_POSITION }
let_it_be(:clump_size) { 300 }
let_it_be(:unclumped) do
(0..clump_size).to_a.map do |i|
create(:issue, project: project, author: user, relative_position: start + (1024 * i))
end
end
let_it_be(:end_clump) do
(0..clump_size).to_a.map do |i|
create(:issue, project: project, author: user, relative_position: max_pos - i)
end
end
let_it_be(:start_clump) do
(0..clump_size).to_a.map do |i|
create(:issue, project: project, author: user, relative_position: min_pos + i)
end
end
def issues_in_position_order
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)
expect { service.execute }.not_to change { issues_in_position_order.map(&:id) }
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
end
expect(gaps).to all(be > RelativePositioning::MIN_GAP)
expect(all_issues.first.relative_position).to be > RelativePositioning::MIN_POSITION
expect(all_issues.last.relative_position).to be < RelativePositioning::MAX_POSITION
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