Skip to content
Snippets Groups Projects
Verified Commit 381468d0 authored by Nick Thomas's avatar Nick Thomas
Browse files

Allow asynchronous rebase operations to be monitored

This MR introduces tracking of the `rebase_jid` for merge requests. As
with `merge_ongoing?`, `rebase_in_progress?` will now return true if a
rebase is proceeding in sidekiq.

After one release, we should remove the Gitaly-based lookup of rebases.
It is much better to track this kind of thing via the database.
parent 9ef0c855
No related branches found
No related tags found
No related merge requests found
Showing with 188 additions and 38 deletions
Loading
Loading
@@ -201,7 +201,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
 
def rebase
RebaseWorker.perform_async(@merge_request.id, current_user.id)
@merge_request.rebase_async(current_user.id)
 
head :ok
end
Loading
Loading
Loading
Loading
@@ -223,7 +223,13 @@ class MergeRequest < ApplicationRecord
end
 
def rebase_in_progress?
strong_memoize(:rebase_in_progress) do
(rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid)) ||
gitaly_rebase_in_progress?
end
# TODO: remove the Gitaly lookup after v12.1, when rebase_jid will be reliable
def gitaly_rebase_in_progress?
strong_memoize(:gitaly_rebase_in_progress) do
# The source project can be deleted
next false unless source_project
 
Loading
Loading
@@ -389,6 +395,26 @@ class MergeRequest < ApplicationRecord
update_column(:merge_jid, jid)
end
 
# Set off a rebase asynchronously, atomically updating the `rebase_jid` of
# the MR so that the status of the operation can be tracked.
def rebase_async(user_id)
transaction do
lock!
raise ActiveRecord::StaleObjectError if !open? || rebase_in_progress?
# Although there is a race between setting rebase_jid here and clearing it
# in the RebaseWorker, it can't do any harm since we check both that the
# attribute is set *and* that the sidekiq job is still running. So a JID
# for a completed RebaseWorker is equivalent to a nil JID.
jid = Sidekiq::Worker.skipping_transaction_check do
RebaseWorker.perform_async(id, user_id)
end
update_column(:rebase_jid, jid)
end
end
def merge_participants
participants = [author]
 
Loading
Loading
Loading
Loading
@@ -15,7 +15,7 @@ module MergeRequests
end
 
def rebase
if merge_request.rebase_in_progress?
if merge_request.gitaly_rebase_in_progress?
log_error('Rebase task canceled: Another rebase is already in progress', save_message_on_model: true)
return false
end
Loading
Loading
@@ -27,6 +27,8 @@ module MergeRequests
log_error(REBASE_ERROR, save_message_on_model: true)
log_error(e.message)
false
ensure
merge_request.update_column(:rebase_jid, nil)
end
end
end
# frozen_string_literal: true
 
# The RebaseWorker must be wrapped in important concurrency code, so should only
# be scheduled via MergeRequest#rebase_async
class RebaseWorker
include ApplicationWorker
 
Loading
Loading
---
title: Allow asynchronous rebase operations to be monitored
merge_request: 29940
author:
type: fixed
# frozen_string_literal: true
class AddMergeRequestRebaseJid < ActiveRecord::Migration[5.1]
DOWNTIME = false
def change
add_column :merge_requests, :rebase_jid, :string
end
end
Loading
Loading
@@ -1989,6 +1989,7 @@ ActiveRecord::Schema.define(version: 20190628185004) do
t.boolean "allow_maintainer_to_push"
t.integer "state_id", limit: 2
t.integer "approvals_before_merge"
t.string "rebase_jid"
t.index ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
t.index ["author_id"], name: "index_merge_requests_on_author_id", using: :btree
t.index ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
Loading
Loading
Loading
Loading
@@ -1485,8 +1485,14 @@ PUT /projects/:id/merge_requests/:merge_request_iid/rebase
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/76/merge_requests/1/rebase
```
 
This is an asynchronous request. The API will return an empty `202 Accepted`
response if the request is enqueued successfully.
This is an asynchronous request. The API will return a `202 Accepted` response
if the request is enqueued successfully, with a response containing:
```json
{
"rebase_in_progress": true
}
```
 
You can poll the [Get single MR](#get-single-mr) endpoint with the
`include_rebase_in_progress` parameter to check the status of the
Loading
Loading
Loading
Loading
@@ -52,7 +52,10 @@ module API
rack_response({ 'message' => '404 Not found' }.to_json, 404)
end
 
rescue_from ::Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError do
rescue_from(
::ActiveRecord::StaleObjectError,
::Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
) do
rack_response({ 'message' => '409 Conflict: Resource lock' }.to_json, 409)
end
 
Loading
Loading
Loading
Loading
@@ -429,9 +429,10 @@ module API
 
authorize_push_to_merge_request!(merge_request)
 
RebaseWorker.perform_async(merge_request.id, current_user.id)
merge_request.rebase_async(current_user.id)
 
status :accepted
present rebase_in_progress: merge_request.rebase_in_progress?
end
 
desc 'List issues that will be closed on merge' do
Loading
Loading
Loading
Loading
@@ -160,6 +160,7 @@ excluded_attributes:
- :milestone_id
- :ref_fetched
- :merge_jid
- :rebase_jid
- :latest_merge_request_diff_id
award_emoji:
- :awardable_id
Loading
Loading
Loading
Loading
@@ -7,6 +7,8 @@ describe MergeRequest do
include ProjectForksHelper
include ReactiveCachingHelpers
 
using RSpec::Parameterized::TableSyntax
subject { create(:merge_request) }
 
describe 'associations' do
Loading
Loading
@@ -1996,6 +1998,47 @@ describe MergeRequest do
end
end
 
describe '#rebase_async' do
let(:merge_request) { create(:merge_request) }
let(:user_id) { double(:user_id) }
let(:rebase_jid) { 'rebase-jid' }
subject(:execute) { merge_request.rebase_async(user_id) }
it 'atomically enqueues a RebaseWorker job and updates rebase_jid' do
expect(RebaseWorker)
.to receive(:perform_async)
.with(merge_request.id, user_id)
.and_return(rebase_jid)
expect(merge_request).to receive(:lock!).and_call_original
execute
expect(merge_request.rebase_jid).to eq(rebase_jid)
end
it 'refuses to enqueue a job if a rebase is in progress' do
merge_request.update_column(:rebase_jid, rebase_jid)
expect(RebaseWorker).not_to receive(:perform_async)
expect(Gitlab::SidekiqStatus)
.to receive(:running?)
.with(rebase_jid)
.and_return(true)
expect { execute }.to raise_error(ActiveRecord::StaleObjectError)
end
it 'refuses to enqueue a job if the MR is not open' do
merge_request.update_column(:state, 'foo')
expect(RebaseWorker).not_to receive(:perform_async)
expect { execute }.to raise_error(ActiveRecord::StaleObjectError)
end
end
describe '#mergeable?' do
let(:project) { create(:project) }
 
Loading
Loading
@@ -2946,40 +2989,64 @@ describe MergeRequest do
end
 
describe '#rebase_in_progress?' do
shared_examples 'checking whether a rebase is in progress' do
let(:repo_path) do
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
subject.source_project.repository.path
end
end
let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") }
where(:rebase_jid, :jid_valid, :result) do
'foo' | true | true
'foo' | false | false
'' | true | false
nil | true | false
end
 
before do
system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{rebase_path} master))
with_them do
let(:merge_request) { create(:merge_request) }
subject { merge_request.rebase_in_progress? }
it do
# Stub out the legacy gitaly implementation
allow(merge_request).to receive(:gitaly_rebase_in_progress?) { false }
allow(Gitlab::SidekiqStatus).to receive(:running?).with(rebase_jid) { jid_valid }
merge_request.rebase_jid = rebase_jid
is_expected.to eq(result)
end
end
end
 
it 'returns true when there is a current rebase directory' do
expect(subject.rebase_in_progress?).to be_truthy
describe '#gitaly_rebase_in_progress?' do
let(:repo_path) do
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
subject.source_project.repository.path
end
end
let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") }
before do
system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{rebase_path} master))
end
 
it 'returns false when there is no rebase directory' do
FileUtils.rm_rf(rebase_path)
it 'returns true when there is a current rebase directory' do
expect(subject.rebase_in_progress?).to be_truthy
end
 
expect(subject.rebase_in_progress?).to be_falsey
end
it 'returns false when there is no rebase directory' do
FileUtils.rm_rf(rebase_path)
 
it 'returns false when the rebase directory has expired' do
time = 20.minutes.ago.to_time
File.utime(time, time, rebase_path)
expect(subject.rebase_in_progress?).to be_falsey
end
 
expect(subject.rebase_in_progress?).to be_falsey
end
it 'returns false when the rebase directory has expired' do
time = 20.minutes.ago.to_time
File.utime(time, time, rebase_path)
 
it 'returns false when the source project has been removed' do
allow(subject).to receive(:source_project).and_return(nil)
expect(subject.rebase_in_progress?).to be_falsey
end
 
expect(subject.rebase_in_progress?).to be_falsey
end
it 'returns false when the source project has been removed' do
allow(subject).to receive(:source_project).and_return(nil)
expect(subject.rebase_in_progress?).to be_falsey
end
end
 
Loading
Loading
Loading
Loading
@@ -2033,6 +2033,9 @@ describe API::MergeRequests do
 
expect(response).to have_gitlab_http_status(202)
expect(RebaseWorker.jobs.size).to eq(1)
expect(merge_request.reload).to be_rebase_in_progress
expect(json_response['rebase_in_progress']).to be(true)
end
 
it 'returns 403 if the user cannot push to the branch' do
Loading
Loading
@@ -2043,6 +2046,16 @@ describe API::MergeRequests do
 
expect(response).to have_gitlab_http_status(403)
end
it 'returns 409 if a rebase is already in progress' do
Sidekiq::Testing.fake! do
merge_request.rebase_async(user.id)
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/rebase", user)
end
expect(response).to have_gitlab_http_status(409)
end
end
 
describe 'Time tracking' do
Loading
Loading
Loading
Loading
@@ -6,10 +6,12 @@ describe MergeRequests::RebaseService do
include ProjectForksHelper
 
let(:user) { create(:user) }
let(:rebase_jid) { 'fake-rebase-jid' }
let(:merge_request) do
create(:merge_request,
create :merge_request,
source_branch: 'feature_conflict',
target_branch: 'master')
target_branch: 'master',
rebase_jid: rebase_jid
end
let(:project) { merge_request.project }
let(:repository) { project.repository.raw }
Loading
Loading
@@ -23,11 +25,11 @@ describe MergeRequests::RebaseService do
describe '#execute' do
context 'when another rebase is already in progress' do
before do
allow(merge_request).to receive(:rebase_in_progress?).and_return(true)
allow(merge_request).to receive(:gitaly_rebase_in_progress?).and_return(true)
end
 
it 'saves the error message' do
subject.execute(merge_request)
service.execute(merge_request)
 
expect(merge_request.reload.merge_error).to eq 'Rebase task canceled: Another rebase is already in progress'
end
Loading
Loading
@@ -36,6 +38,13 @@ describe MergeRequests::RebaseService do
expect(service.execute(merge_request)).to match(status: :error,
message: described_class::REBASE_ERROR)
end
it 'clears rebase_jid' do
expect { service.execute(merge_request) }
.to change { merge_request.rebase_jid }
.from(rebase_jid)
.to(nil)
end
end
 
shared_examples 'sequence of failure and success' do
Loading
Loading
@@ -43,14 +52,19 @@ describe MergeRequests::RebaseService do
allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong')
 
service.execute(merge_request)
merge_request.reload
 
expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR
expect(merge_request.reload.merge_error).to eq(described_class::REBASE_ERROR)
expect(merge_request.rebase_jid).to eq(nil)
 
allow(repository).to receive(:gitaly_operation_client).and_call_original
merge_request.update!(rebase_jid: rebase_jid)
 
service.execute(merge_request)
merge_request.reload
 
expect(merge_request.reload.merge_error).to eq nil
expect(merge_request.merge_error).to eq(nil)
expect(merge_request.rebase_jid).to eq(nil)
end
end
 
Loading
Loading
@@ -72,7 +86,7 @@ describe MergeRequests::RebaseService do
it 'saves a generic error message' do
subject.execute(merge_request)
 
expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR
expect(merge_request.reload.merge_error).to eq(described_class::REBASE_ERROR)
end
 
it 'returns an error' do
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment