Commit 8c4c2368 authored by Patrick Bajao's avatar Patrick Bajao

Resolve merge request todos asynchrnously on update

This is one of the changes to reduce the number of SQL queries
when `MergeRequests::UpdateService`.

The strategy is to delegate todo resolution (marking pending
todo as done) to Sidekiq. This way, SQL queries needed for it
won't need to run synchronously on update.
parent 2d19c7b9
# frozen_string_literal: true
module MergeRequests
class ResolveTodosService
include BaseServiceUtility
def initialize(merge_request, user)
@merge_request = merge_request
@user = user
end
def async_execute
if Feature.enabled?(:resolve_merge_request_todos_async, merge_request.target_project, default_enabled: :yaml)
MergeRequests::ResolveTodosWorker.perform_async(merge_request.id, user.id)
else
execute
end
end
def execute
todo_service.resolve_todos_for_target(merge_request, user)
end
private
attr_reader :merge_request, :user
end
end
...@@ -147,7 +147,11 @@ module MergeRequests ...@@ -147,7 +147,11 @@ module MergeRequests
def resolve_todos(merge_request, old_labels, old_assignees, old_reviewers) def resolve_todos(merge_request, old_labels, old_assignees, old_reviewers)
return unless has_changes?(merge_request, old_labels: old_labels, old_assignees: old_assignees, old_reviewers: old_reviewers) return unless has_changes?(merge_request, old_labels: old_labels, old_assignees: old_assignees, old_reviewers: old_reviewers)
todo_service.resolve_todos_for_target(merge_request, current_user) service_user = current_user
merge_request.run_after_commit_or_now do
::MergeRequests::ResolveTodosService.new(merge_request, service_user).async_execute
end
end end
def handle_target_branch_change(merge_request) def handle_target_branch_change(merge_request)
......
...@@ -1908,6 +1908,14 @@ ...@@ -1908,6 +1908,14 @@
:weight: 1 :weight: 1
:idempotent: true :idempotent: true
:tags: [] :tags: []
- :name: merge_requests_resolve_todos
:feature_category: :code_review
:has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: metrics_dashboard_prune_old_annotations - :name: metrics_dashboard_prune_old_annotations
:feature_category: :metrics :feature_category: :metrics
:has_external_dependencies: :has_external_dependencies:
......
# frozen_string_literal: true
class MergeRequests::ResolveTodosWorker
include ApplicationWorker
feature_category :code_review
urgency :high
deduplicate :until_executed
idempotent!
def perform(merge_request_id, user_id)
merge_request = MergeRequest.find(merge_request_id)
user = User.find(user_id)
MergeRequests::ResolveTodosService.new(merge_request, user).execute
rescue ActiveRecord::RecordNotFound
end
end
---
title: Resolve merge request todos asynchronously on update
merge_request: 58647
author:
type: performance
---
name: resolve_merge_request_todos_async
introduced_by_url:
rollout_issue_url:
milestone: '13.11'
type: development
group: group::code review
default_enabled: false
...@@ -218,6 +218,8 @@ ...@@ -218,6 +218,8 @@
- 1 - 1
- - merge_requests_handle_assignees_change - - merge_requests_handle_assignees_change
- 1 - 1
- - merge_requests_resolve_todos
- 1
- - metrics_dashboard_prune_old_annotations - - metrics_dashboard_prune_old_annotations
- 1 - 1
- - metrics_dashboard_sync_dashboards - - metrics_dashboard_sync_dashboards
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe MergeRequests::ResolveTodosService do
let_it_be(:merge_request) { create(:merge_request) }
let_it_be(:user) { create(:user) }
let(:service) { described_class.new(merge_request, user) }
describe '#async_execute' do
def async_execute
service.async_execute
end
it 'performs MergeRequests::ResolveTodosWorker asynchronously' do
expect(MergeRequests::ResolveTodosWorker)
.to receive(:perform_async)
.with(
merge_request.id,
user.id
)
async_execute
end
context 'when resolve_merge_request_todos_async feature is disabled' do
before do
stub_feature_flags(resolve_merge_request_todos_async: false)
end
it 'calls #execute' do
expect(service).to receive(:execute)
async_execute
end
end
end
describe '#execute' do
it 'marks pending todo as done' do
pending_todo = create(:todo, :pending, user: user, project: merge_request.project, target: merge_request)
service.execute
expect(pending_todo.reload).to be_done
end
end
end
...@@ -590,48 +590,54 @@ RSpec.describe MergeRequests::UpdateService, :mailer do ...@@ -590,48 +590,54 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
let!(:pending_todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) } let!(:pending_todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
context 'when the title change' do context 'when the title change' do
before do it 'calls MergeRequest::ResolveTodosService#async_execute' do
update_merge_request({ title: 'New title' }) expect_next_instance_of(MergeRequests::ResolveTodosService, merge_request, user) do |service|
end expect(service).to receive(:async_execute)
end
it 'marks pending todos as done' do update_merge_request({ title: 'New title' })
expect(pending_todo.reload).to be_done
end end
it 'does not create any new todos' do it 'does not create any new todos' do
update_merge_request({ title: 'New title' })
expect(Todo.count).to eq(1) expect(Todo.count).to eq(1)
end end
end end
context 'when the description change' do context 'when the description change' do
before do it 'calls MergeRequest::ResolveTodosService#async_execute' do
update_merge_request({ description: "Also please fix #{user2.to_reference} #{user3.to_reference}" }) expect_next_instance_of(MergeRequests::ResolveTodosService, merge_request, user) do |service|
end expect(service).to receive(:async_execute)
end
it 'marks pending todos as done' do update_merge_request({ description: "Also please fix #{user2.to_reference} #{user3.to_reference}" })
expect(pending_todo.reload).to be_done
end end
it 'creates only 1 new todo' do it 'creates only 1 new todo' do
update_merge_request({ description: "Also please fix #{user2.to_reference} #{user3.to_reference}" })
expect(Todo.count).to eq(2) expect(Todo.count).to eq(2)
end end
end end
context 'when is reassigned' do context 'when is reassigned' do
before do it 'calls MergeRequest::ResolveTodosService#async_execute' do
update_merge_request({ assignee_ids: [user2.id] }) expect_next_instance_of(MergeRequests::ResolveTodosService, merge_request, user) do |service|
end expect(service).to receive(:async_execute)
end
it 'marks previous assignee pending todos as done' do update_merge_request({ assignee_ids: [user2.id] })
expect(pending_todo.reload).to be_done
end end
end end
context 'when reviewers gets changed' do context 'when reviewers gets changed' do
it 'marks pending todo as done' do it 'calls MergeRequest::ResolveTodosService#async_execute' do
update_merge_request({ reviewer_ids: [user2.id] }) expect_next_instance_of(MergeRequests::ResolveTodosService, merge_request, user) do |service|
expect(service).to receive(:async_execute)
end
expect(pending_todo.reload).to be_done update_merge_request({ reviewer_ids: [user2.id] })
end end
it 'creates a pending todo for new review request' do it 'creates a pending todo for new review request' do
...@@ -709,10 +715,12 @@ RSpec.describe MergeRequests::UpdateService, :mailer do ...@@ -709,10 +715,12 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
end end
end end
it 'marks pending todos as done' do it 'calls MergeRequests::ResolveTodosService#async_execute' do
update_merge_request({ milestone: create(:milestone, project: project) }) expect_next_instance_of(MergeRequests::ResolveTodosService, merge_request, user) do |service|
expect(service).to receive(:async_execute)
end
expect(pending_todo.reload).to be_done update_merge_request({ milestone: create(:milestone, project: project) })
end end
it 'sends notifications for subscribers of changed milestone', :sidekiq_might_not_need_inline do it 'sends notifications for subscribers of changed milestone', :sidekiq_might_not_need_inline do
...@@ -726,17 +734,19 @@ RSpec.describe MergeRequests::UpdateService, :mailer do ...@@ -726,17 +734,19 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
end end
context 'when the labels change' do context 'when the labels change' do
before do it 'calls MergeRequests::ResolveTodosService#async_execute' do
travel_to(1.minute.from_now) do expect_next_instance_of(MergeRequests::ResolveTodosService, merge_request, user) do |service|
update_merge_request({ label_ids: [label.id] }) expect(service).to receive(:async_execute)
end end
end
it 'marks pending todos as done' do update_merge_request({ label_ids: [label.id] })
expect(pending_todo.reload).to be_done
end end
it 'updates updated_at' do it 'updates updated_at' do
travel_to(1.minute.from_now) do
update_merge_request({ label_ids: [label.id] })
end
expect(merge_request.reload.updated_at).to be > Time.current expect(merge_request.reload.updated_at).to be > Time.current
end end
end end
...@@ -751,24 +761,26 @@ RSpec.describe MergeRequests::UpdateService, :mailer do ...@@ -751,24 +761,26 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
end end
context 'when the target branch change' do context 'when the target branch change' do
before do it 'calls MergeRequests::ResolveTodosService#async_execute' do
update_merge_request({ target_branch: 'target' }) expect_next_instance_of(MergeRequests::ResolveTodosService, merge_request, user) do |service|
end expect(service).to receive(:async_execute)
end
it 'marks pending todos as done' do update_merge_request({ target_branch: 'target' })
expect(pending_todo.reload).to be_done
end end
end end
context 'when auto merge is enabled and target branch changed' do context 'when auto merge is enabled and target branch changed' do
before do before do
AutoMergeService.new(project, user, { sha: merge_request.diff_head_sha }).execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS) AutoMergeService.new(project, user, { sha: merge_request.diff_head_sha }).execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
update_merge_request({ target_branch: 'target' })
end end
it 'marks pending todos as done' do it 'calls MergeRequests::ResolveTodosService#async_execute' do
expect(pending_todo.reload).to be_done expect_next_instance_of(MergeRequests::ResolveTodosService, merge_request, user) do |service|
expect(service).to receive(:async_execute)
end
update_merge_request({ target_branch: 'target' })
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe MergeRequests::ResolveTodosWorker do
include AfterNextHelpers
let_it_be(:merge_request) { create(:merge_request) }
let_it_be(:user) { create(:user) }
let(:worker) { described_class.new }
it_behaves_like 'an idempotent worker' do
let(:job_args) { [merge_request.id, user.id] }
end
describe '#perform' do
it 'calls MergeRequests::ResolveTodosService#execute' do
expect_next(::MergeRequests::ResolveTodosService, merge_request, user)
.to receive(:execute)
worker.perform(merge_request.id, user.id)
end
context 'with a non-existing merge request' do
it 'does nothing' do
expect(::MergeRequests::ResolveTodosService).not_to receive(:new)
worker.perform(non_existing_record_id, user.id)
end
end
context 'with a non-existing user' do
it 'does nothing' do
expect(::MergeRequests::ResolveTodosService).not_to receive(:new)
worker.perform(merge_request.id, non_existing_record_id)
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