diff options
Diffstat (limited to 'app/workers/issues')
-rw-r--r-- | app/workers/issues/placement_worker.rb | 67 | ||||
-rw-r--r-- | app/workers/issues/rebalancing_worker.rb | 51 | ||||
-rw-r--r-- | app/workers/issues/reschedule_stuck_issue_rebalances_worker.rb | 36 |
3 files changed, 154 insertions, 0 deletions
diff --git a/app/workers/issues/placement_worker.rb b/app/workers/issues/placement_worker.rb new file mode 100644 index 00000000000..ec29a754128 --- /dev/null +++ b/app/workers/issues/placement_worker.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Issues + class PlacementWorker + include ApplicationWorker + + data_consistency :always + + sidekiq_options retry: 3 + + idempotent! + deduplicate :until_executed, including_scheduled: true + feature_category :team_planning + urgency :high + worker_resource_boundary :cpu + weight 2 + + # Move at most the most recent 100 issues + QUERY_LIMIT = 100 + + # rubocop: disable CodeReuse/ActiveRecord + def perform(issue_id, project_id = nil) + issue = find_issue(issue_id, project_id) + return unless issue + + # Temporary disable moving null elements because of performance problems + # For more information check https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4321 + return if issue.blocked_for_repositioning? + + # Move the oldest 100 unpositioned items to the end. + # This is to deal with out-of-order execution of the worker, + # while preserving creation order. + to_place = Issue + .relative_positioning_query_base(issue) + .with_null_relative_position + .order({ created_at: :asc }, { id: :asc }) + .limit(QUERY_LIMIT + 1) + .to_a + + leftover = to_place.pop if to_place.count > QUERY_LIMIT + + Issue.move_nulls_to_end(to_place) + Issues::BaseService.new(project: nil).rebalance_if_needed(to_place.max_by(&:relative_position)) + Issues::PlacementWorker.perform_async(nil, leftover.project_id) if leftover.present? + rescue RelativePositioning::NoSpaceLeft => e + Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id, project_id: project_id) + Issues::RebalancingWorker.perform_async(nil, *root_namespace_id_to_rebalance(issue, project_id)) + end + + def find_issue(issue_id, project_id) + return Issue.id_in(issue_id).take if issue_id + + project = Project.id_in(project_id).take + return unless project + + project.issues.take + end + # rubocop: enable CodeReuse/ActiveRecord + + private + + def root_namespace_id_to_rebalance(issue, project_id) + project_id = project_id.presence || issue.project_id + Project.find(project_id)&.self_or_root_group_ids + end + end +end diff --git a/app/workers/issues/rebalancing_worker.rb b/app/workers/issues/rebalancing_worker.rb new file mode 100644 index 00000000000..466617d9fa1 --- /dev/null +++ b/app/workers/issues/rebalancing_worker.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Issues + class RebalancingWorker + include ApplicationWorker + + data_consistency :always + + sidekiq_options retry: 3 + + idempotent! + urgency :low + feature_category :team_planning + deduplicate :until_executed, including_scheduled: true + + def perform(ignore = nil, project_id = nil, root_namespace_id = nil) + # we need to have exactly one of the project_id and root_namespace_id params be non-nil + raise ArgumentError, "Expected only one of the params project_id: #{project_id} and root_namespace_id: #{root_namespace_id}" if project_id && root_namespace_id + return if project_id.nil? && root_namespace_id.nil? + + # pull the projects collection to be rebalanced either the project if namespace is not a group(i.e. user namesapce) + # or the root namespace, this also makes the worker backward compatible with previous version where a project_id was + # passed as the param + projects_to_rebalance = projects_collection(project_id, root_namespace_id) + + # something might have happened with the namespace between scheduling the worker and actually running it, + # maybe it was removed. + if projects_to_rebalance.blank? + Gitlab::ErrorTracking.log_exception( + ArgumentError.new("Projects to be rebalanced not found for arguments: project_id #{project_id}, root_namespace_id: #{root_namespace_id}"), + { project_id: project_id, root_namespace_id: root_namespace_id }) + + return + end + + Issues::RelativePositionRebalancingService.new(projects_to_rebalance).execute + rescue Issues::RelativePositionRebalancingService::TooManyConcurrentRebalances => e + Gitlab::ErrorTracking.log_exception(e, root_namespace_id: root_namespace_id, project_id: project_id) + end + + private + + def projects_collection(project_id, root_namespace_id) + # we can have either project_id(older version) or project_id if project is part of a user namespace and not a group + # or root_namespace_id(newer version) never both. + return Project.id_in([project_id]) if project_id + + Namespace.find_by_id(root_namespace_id)&.all_projects + end + end +end diff --git a/app/workers/issues/reschedule_stuck_issue_rebalances_worker.rb b/app/workers/issues/reschedule_stuck_issue_rebalances_worker.rb new file mode 100644 index 00000000000..d1759589cc0 --- /dev/null +++ b/app/workers/issues/reschedule_stuck_issue_rebalances_worker.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Issues + class RescheduleStuckIssueRebalancesWorker + include ApplicationWorker + include CronjobQueue + + data_consistency :sticky + + idempotent! + urgency :low + feature_category :team_planning + deduplicate :until_executed, including_scheduled: true + + def perform + namespace_ids, project_ids = ::Gitlab::Issues::Rebalancing::State.fetch_rebalancing_groups_and_projects + + return if namespace_ids.blank? && project_ids.blank? + + namespaces = Namespace.id_in(namespace_ids) + projects = Project.id_in(project_ids) + + IssueRebalancingWorker.bulk_perform_async_with_contexts( + namespaces, + arguments_proc: -> (namespace) { [nil, nil, namespace.id] }, + context_proc: -> (namespace) { { namespace: namespace } } + ) + + IssueRebalancingWorker.bulk_perform_async_with_contexts( + projects, + arguments_proc: -> (project) { [nil, project.id, nil] }, + context_proc: -> (project) { { project: project } } + ) + end + end +end |