diff options
Diffstat (limited to 'app/services/issue_rebalancing_service.rb')
-rw-r--r-- | app/services/issue_rebalancing_service.rb | 71 |
1 files changed, 71 insertions, 0 deletions
diff --git a/app/services/issue_rebalancing_service.rb b/app/services/issue_rebalancing_service.rb new file mode 100644 index 00000000000..4138c6441c8 --- /dev/null +++ b/app/services/issue_rebalancing_service.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +class IssueRebalancingService + MAX_ISSUE_COUNT = 10_000 + TooManyIssues = Class.new(StandardError) + + def initialize(issue) + @issue = issue + @base = Issue.relative_positioning_query_base(issue) + end + + def execute + gates = [issue.project, issue.project.group].compact + return unless gates.any? { |gate| Feature.enabled?(:rebalance_issues, gate) } + + raise TooManyIssues, "#{issue_count} issues" if issue_count > MAX_ISSUE_COUNT + + start = RelativePositioning::START_POSITION - (gaps / 2) * gap_size + + Issue.transaction do + indexed_ids.each_slice(100) { |pairs| assign_positions(start, pairs) } + end + end + + private + + attr_reader :issue, :base + + # rubocop: disable CodeReuse/ActiveRecord + def indexed_ids + base.reorder(:relative_position, :id).pluck(:id).each_with_index + end + # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + 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") + 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 + # rubocop: enable CodeReuse/ActiveRecord + + def issue_count + @issue_count ||= base.count + end + + def gaps + issue_count - 1 + end + + def gap_size + # We could try to split the available range over the number of gaps we need, + # but IDEAL_DISTANCE * MAX_ISSUE_COUNT is only 0.1% of the available range, + # so we are guaranteed not to exhaust it by using this static value. + # + # If we raise MAX_ISSUE_COUNT or IDEAL_DISTANCE significantly, this may + # change! + RelativePositioning::IDEAL_DISTANCE + end +end |