Commit 38f7f31a authored by Robert Speicher's avatar Robert Speicher

Merge branch '748-update-mirror' into 'master'

Fix race condition with UpdateMirrorWorker Lease

Any time we modify the project import state to an in progress we're going to execute the sidekiq job not lease to try to obtain in this workflow. This fixes the UI problem when an user try to update a mirror just inside a lease timeout.

Closes #748 

See merge request !641
parents ca8b260a 89c0528c
...@@ -11,6 +11,9 @@ v 8.11.0 (unreleased) ...@@ -11,6 +11,9 @@ v 8.11.0 (unreleased)
- [Elastic] Significant improvement of global search performance - [Elastic] Significant improvement of global search performance
- [Fix] Push rules check existing commits in some cases - [Fix] Push rules check existing commits in some cases
v 8.10.6
- Fix race condition with UpdateMirrorWorker Lease
v 8.10.5 v 8.10.5
- Used cached value of project count in `Elastic::RepositoriesSearch` to reduce DB load. !637 - Used cached value of project count in `Elastic::RepositoriesSearch` to reduce DB load. !637
......
...@@ -603,7 +603,7 @@ class Project < ActiveRecord::Base ...@@ -603,7 +603,7 @@ class Project < ActiveRecord::Base
mirror_updated? && self.mirror_last_successful_update_at mirror_updated? && self.mirror_last_successful_update_at
end end
def update_mirror(delay: 0) def update_mirror
return unless mirror? && repository_exists? return unless mirror? && repository_exists?
return if import_in_progress? return if import_in_progress?
...@@ -614,7 +614,7 @@ class Project < ActiveRecord::Base ...@@ -614,7 +614,7 @@ class Project < ActiveRecord::Base
import_start import_start
end end
RepositoryUpdateMirrorWorker.perform_in(delay, self.id) RepositoryUpdateMirrorWorker.perform_async(self.id)
end end
def has_remote_mirror? def has_remote_mirror?
......
class RepositoryUpdateMirrorDispatchWorker
include Sidekiq::Worker
LEASE_TIMEOUT = 5.minutes
sidekiq_options queue: :gitlab_shell
attr_accessor :project, :repository, :current_user
def perform(project_id)
return unless try_obtain_lease(project_id)
@project = Project.find_by_id(project_id)
return unless project
project.update_mirror
end
private
def try_obtain_lease(project_id)
# Using 5 minutes timeout based on the 95th percent of timings (currently max of 25 minutes)
lease = ::Gitlab::ExclusiveLease.new("repository_update_mirror_dispatcher:#{project_id}", timeout: LEASE_TIMEOUT)
lease.try_obtain
end
end
...@@ -4,16 +4,12 @@ class RepositoryUpdateMirrorWorker ...@@ -4,16 +4,12 @@ class RepositoryUpdateMirrorWorker
include Sidekiq::Worker include Sidekiq::Worker
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
LEASE_TIMEOUT = 300
sidekiq_options queue: :gitlab_shell sidekiq_options queue: :gitlab_shell
attr_accessor :project, :repository, :current_user attr_accessor :project, :repository, :current_user
def perform(project_id) def perform(project_id)
begin begin
return unless try_obtain_lease(project_id)
@project = Project.find(project_id) @project = Project.find(project_id)
@current_user = @project.mirror_user || @project.creator @current_user = @project.mirror_user || @project.creator
...@@ -30,12 +26,4 @@ class RepositoryUpdateMirrorWorker ...@@ -30,12 +26,4 @@ class RepositoryUpdateMirrorWorker
raise UpdateMirrorError, "#{ex.class}: #{Gitlab::UrlSanitizer.sanitize(ex.message)}" raise UpdateMirrorError, "#{ex.class}: #{Gitlab::UrlSanitizer.sanitize(ex.message)}"
end end
end end
private
def try_obtain_lease(project_id)
# Using 5 minutes timeout based on the 95th percent of timings (currently max of 25 seconds)
lease = ::Gitlab::ExclusiveLease.new("repository_update_mirror:#{project_id}", timeout: LEASE_TIMEOUT)
lease.try_obtain
end
end end
...@@ -9,7 +9,7 @@ class UpdateAllMirrorsWorker ...@@ -9,7 +9,7 @@ class UpdateAllMirrorsWorker
fail_stuck_mirrors! fail_stuck_mirrors!
Project.mirror.find_each(batch_size: 200) do |project| Project.mirror.find_each(batch_size: 200) do |project|
project.update_mirror(delay: rand(30.minutes)) RepositoryUpdateMirrorDispatchWorker.perform_in(rand(30.minutes), project.id)
end end
end end
......
require 'spec_helper'
feature 'Project mirror', feature: true do
let(:project) { create(:project, :mirror, :import_finished, creator: user, name: 'Victorialand') }
let(:user) { create(:user) }
describe 'On a project', js: true do
before do
project.team << [user, :master]
login_as user
visit namespace_project_mirror_path(project.namespace, project)
end
describe 'pressing "Update now"' do
it 'returns with the project updating (job enqueued)' do
Sidekiq::Testing.fake! { click_link('Update Now') }
expect(page).to have_content('Updating')
end
end
end
end
require 'rails_helper'
describe RepositoryUpdateMirrorDispatchWorker do
describe '#perform' do
it 'executes project#update_mirror if can obtain a lease' do
allow_any_instance_of(Gitlab::ExclusiveLease)
.to receive(:try_obtain).and_return(true)
expect_any_instance_of(Project).to receive(:update_mirror)
project = create(:empty_project, :mirror)
described_class.new.perform(project.id)
end
it 'just returns if cannot obtain a lease' do
allow_any_instance_of(Gitlab::ExclusiveLease)
.to receive(:try_obtain).and_return(false)
expect_any_instance_of(Project).not_to receive(:update_mirror)
project = create(:empty_project, :mirror)
described_class.new.perform(project.id)
end
end
end
...@@ -2,43 +2,22 @@ require 'rails_helper' ...@@ -2,43 +2,22 @@ require 'rails_helper'
describe RepositoryUpdateMirrorWorker do describe RepositoryUpdateMirrorWorker do
describe '#perform' do describe '#perform' do
it "just returns if cannot obtain a lease" do it 'sets import as finished when update mirror service executes successfully' do
worker = described_class.new project = create(:empty_project, :mirror)
project_id = 15
allow_any_instance_of(Gitlab::ExclusiveLease) expect_any_instance_of(Projects::UpdateMirrorService).to receive(:execute).and_return(status: :success)
.to receive(:try_obtain).and_return(false)
expect(Projects::UpdateMirrorService).not_to receive(:execute) expect { described_class.new.perform(project.id) }
.to change { project.reload.import_status }.to('finished')
worker.perform(project_id)
end end
context "when obtain the lease" do it 'sets import as failed when update mirror service executes with errors' do
before do project = create(:empty_project, :mirror)
allow_any_instance_of(Gitlab::ExclusiveLease)
.to receive(:try_obtain).and_return(true)
end
it "sets import as finished when update mirror service executes successfully" do
project = create(:empty_project, :mirror)
expect_any_instance_of(Projects::UpdateMirrorService).to receive(:execute).and_return(status: :success)
expect do
described_class.new.perform(project.id)
end.to change { project.reload.import_status }.to("finished")
end
it "sets import as failed when update mirror service executes with errors" do
project = create(:empty_project, :mirror)
expect_any_instance_of(Projects::UpdateMirrorService).to receive(:execute).and_return(status: :error, message: 'fail!') expect_any_instance_of(Projects::UpdateMirrorService).to receive(:execute).and_return(status: :error, message: 'fail!')
expect do expect { described_class.new.perform(project.id) }
described_class.new.perform(project.id) .to change { project.reload.import_status }.to('failed')
end.to change { project.reload.import_status }.to("failed")
end
end end
end end
end end
...@@ -15,14 +15,14 @@ describe UpdateAllMirrorsWorker do ...@@ -15,14 +15,14 @@ describe UpdateAllMirrorsWorker do
worker.perform worker.perform
end end
it 'updates all mirrored Projects' do it 'enqueue a job on all mirrored Projects' do
worker = described_class.new worker = described_class.new
create(:empty_project, :mirror) mirror = create(:empty_project, :mirror)
create(:empty_project) create(:empty_project)
expect(worker).to receive(:rand).with(30.minutes).and_return(10) expect(worker).to receive(:rand).with(30.minutes).and_return(10)
expect_any_instance_of(Project).to receive(:update_mirror).with(delay: 10).once expect(RepositoryUpdateMirrorDispatchWorker).to receive(:perform_in).with(10, mirror.id)
worker.perform worker.perform
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