Commit f865b1b4 authored by Tiago Botelho's avatar Tiago Botelho

Backports EE mirror stuck handling feature...

Backports EE mirror stuck handling feature (https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2628) to CE
parent 4a2a6d52
...@@ -144,8 +144,6 @@ end ...@@ -144,8 +144,6 @@ end
# State machine # State machine
gem 'state_machines-activerecord', '~> 0.4.0' gem 'state_machines-activerecord', '~> 0.4.0'
# Run events after state machine commits
gem 'after_commit_queue', '~> 1.3.0'
# Issue tags # Issue tags
gem 'acts-as-taggable-on', '~> 4.0' gem 'acts-as-taggable-on', '~> 4.0'
......
...@@ -46,8 +46,6 @@ GEM ...@@ -46,8 +46,6 @@ GEM
ice_nine (~> 0.11.0) ice_nine (~> 0.11.0)
memoizable (~> 0.4.0) memoizable (~> 0.4.0)
addressable (2.3.8) addressable (2.3.8)
after_commit_queue (1.3.0)
activerecord (>= 3.0)
akismet (2.0.0) akismet (2.0.0)
allocations (1.0.5) allocations (1.0.5)
arel (6.0.4) arel (6.0.4)
...@@ -963,7 +961,6 @@ DEPENDENCIES ...@@ -963,7 +961,6 @@ DEPENDENCIES
activerecord_sane_schema_dumper (= 0.2) activerecord_sane_schema_dumper (= 0.2)
acts-as-taggable-on (~> 4.0) acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8) addressable (~> 2.3.8)
after_commit_queue (~> 1.3.0)
akismet (~> 2.0) akismet (~> 2.0)
allocations (~> 1.0) allocations (~> 1.0)
asana (~> 0.6.0) asana (~> 0.6.0)
......
...@@ -369,7 +369,10 @@ class Project < ActiveRecord::Base ...@@ -369,7 +369,10 @@ class Project < ActiveRecord::Base
state :failed state :failed
after_transition [:none, :finished, :failed] => :scheduled do |project, _| after_transition [:none, :finished, :failed] => :scheduled do |project, _|
project.run_after_commit { add_import_job } project.run_after_commit do
job_id = add_import_job
update(import_jid: job_id) if job_id
end
end end
after_transition started: :finished do |project, _| after_transition started: :finished do |project, _|
...@@ -524,17 +527,26 @@ class Project < ActiveRecord::Base ...@@ -524,17 +527,26 @@ class Project < ActiveRecord::Base
def add_import_job def add_import_job
job_id = job_id =
if forked? if forked?
RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path, RepositoryForkWorker.perform_async(id,
forked_from_project.repository_storage_path,
forked_from_project.full_path, forked_from_project.full_path,
self.namespace.full_path) self.namespace.full_path)
else else
RepositoryImportWorker.perform_async(self.id) RepositoryImportWorker.perform_async(self.id)
end end
log_import_activity(job_id)
job_id
end
def log_import_activity(job_id, type: :import)
job_type = type.to_s.capitalize
if job_id if job_id
Rails.logger.info "Import job started for #{full_path} with job ID #{job_id}" Rails.logger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.")
else else
Rails.logger.error "Import job failed to start for #{full_path}" Rails.logger.error("#{job_type} job failed to create for #{full_path}.")
end end
end end
...@@ -543,6 +555,7 @@ class Project < ActiveRecord::Base ...@@ -543,6 +555,7 @@ class Project < ActiveRecord::Base
ProjectCacheWorker.perform_async(self.id) ProjectCacheWorker.perform_async(self.id)
end end
update(import_error: nil)
remove_import_data remove_import_data
end end
......
...@@ -5,14 +5,17 @@ class RepositoryForkWorker ...@@ -5,14 +5,17 @@ class RepositoryForkWorker
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
def perform(project_id, forked_from_repository_storage_path, source_path, target_path) def perform(project_id, forked_from_repository_storage_path, source_path, target_path)
project = Project.find(project_id)
return unless start_fork(project)
Gitlab::Metrics.add_event(:fork_repository, Gitlab::Metrics.add_event(:fork_repository,
source_path: source_path, source_path: source_path,
target_path: target_path) target_path: target_path)
project = Project.find(project_id)
project.import_start
result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path, result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path,
project.repository_storage_path, target_path) project.repository_storage_path, target_path)
raise ForkError, "Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}" unless result raise ForkError, "Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}" unless result
...@@ -33,6 +36,13 @@ class RepositoryForkWorker ...@@ -33,6 +36,13 @@ class RepositoryForkWorker
private private
def start_fork(project)
return true if project.import_start
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.")
false
end
def fail_fork(project, message) def fail_fork(project, message)
Rails.logger.error(message) Rails.logger.error(message)
project.mark_import_as_failed(message) project.mark_import_as_failed(message)
......
...@@ -4,23 +4,18 @@ class RepositoryImportWorker ...@@ -4,23 +4,18 @@ class RepositoryImportWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_EXPIRATION sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
attr_accessor :project, :current_user
def perform(project_id) def perform(project_id)
@project = Project.find(project_id) project = Project.find(project_id)
@current_user = @project.creator
project.import_start return unless start_import(project)
Gitlab::Metrics.add_event(:import_repository, Gitlab::Metrics.add_event(:import_repository,
import_url: @project.import_url, import_url: project.import_url,
path: @project.full_path) path: project.full_path)
project.update_columns(import_jid: self.jid, import_error: nil)
result = Projects::ImportService.new(project, current_user).execute result = Projects::ImportService.new(project, project.creator).execute
raise ImportError, result[:message] if result[:status] == :error raise ImportError, result[:message] if result[:status] == :error
project.repository.after_import project.repository.after_import
...@@ -37,6 +32,13 @@ class RepositoryImportWorker ...@@ -37,6 +32,13 @@ class RepositoryImportWorker
private private
def start_import(project)
return true if project.import_start
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.")
false
end
def fail_import(project, message) def fail_import(project, message)
project.mark_import_as_failed(message) project.mark_import_as_failed(message)
end end
......
...@@ -2,36 +2,60 @@ class StuckImportJobsWorker ...@@ -2,36 +2,60 @@ class StuckImportJobsWorker
include Sidekiq::Worker include Sidekiq::Worker
include CronjobQueue include CronjobQueue
IMPORT_EXPIRATION = 15.hours.to_i IMPORT_JOBS_EXPIRATION = 15.hours.to_i
def perform def perform
stuck_projects.find_in_batches(batch_size: 500) do |group| projects_without_jid_count = mark_projects_without_jid_as_failed!
projects_with_jid_count = mark_projects_with_jid_as_failed!
Gitlab::Metrics.add_event(:stuck_import_jobs,
projects_without_jid_count: projects_without_jid_count,
projects_with_jid_count: projects_with_jid_count)
end
private
def mark_projects_without_jid_as_failed!
started_projects_without_jid.each do |project|
project.mark_import_as_failed(error_message)
end.count
end
def mark_projects_with_jid_as_failed!
completed_jids_count = 0
started_projects_with_jid.find_in_batches(batch_size: 500) do |group|
jids = group.map(&:import_jid) jids = group.map(&:import_jid)
# Find the jobs that aren't currently running or that exceeded the threshold. # Find the jobs that aren't currently running or that exceeded the threshold.
completed_jids = Gitlab::SidekiqStatus.completed_jids(jids) completed_jids = Gitlab::SidekiqStatus.completed_jids(jids).to_set
if completed_jids.any? if completed_jids.any?
completed_ids = group.select { |project| completed_jids.include?(project.import_jid) }.map(&:id) completed_jids_count += completed_jids.count
group.each do |project|
fail_batch!(completed_jids, completed_ids) project.mark_import_as_failed(error_message) if completed_jids.include?(project.import_jid)
end end
Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.to_a.join(', ')}")
end end
end end
private completed_jids_count
end
def stuck_projects def started_projects
Project.select('id, import_jid').with_import_status(:started).where.not(import_jid: nil) Project.with_import_status(:started)
end end
def fail_batch!(completed_jids, completed_ids) def started_projects_with_jid
Project.where(id: completed_ids).update_all(import_status: 'failed', import_error: error_message) started_projects.where.not(import_jid: nil)
end
Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.join(', ')}") def started_projects_without_jid
started_projects.where(import_jid: nil)
end end
def error_message def error_message
"Import timed out. Import took longer than #{IMPORT_EXPIRATION} seconds" "Import timed out. Import took longer than #{IMPORT_JOBS_EXPIRATION} seconds"
end end
end end
module AfterCommitQueue
extend ActiveSupport::Concern
included do
after_commit :_run_after_commit_queue
after_rollback :_clear_after_commit_queue
end
def run_after_commit(method = nil, &block)
_after_commit_queue << proc { self.send(method) } if method
_after_commit_queue << block if block
true
end
protected
def _run_after_commit_queue
while action = _after_commit_queue.pop
self.instance_eval(&action)
end
end
def _after_commit_queue
@after_commit_queue ||= []
end
def _clear_after_commit_queue
_after_commit_queue.clear
end
end
...@@ -90,9 +90,14 @@ module Gitlab ...@@ -90,9 +90,14 @@ module Gitlab
# #
# Returns an array of completed JIDs # Returns an array of completed JIDs
def self.completed_jids(job_ids) def self.completed_jids(job_ids)
Sidekiq.redis do |redis| statuses = job_status(job_ids)
job_ids.reject { |jid| redis.exists(key_for(jid)) }
completed = []
job_ids.zip(statuses).each do |job_id, status|
completed << job_id unless status
end end
completed
end end
def self.key_for(jid) def self.key_for(jid)
......
require 'spec_helper'
describe AfterCommitQueue do
it 'runs after transaction is committed' do
called = false
test_proc = proc { called = true }
project = build(:project)
project.run_after_commit(&test_proc)
project.save
expect(called).to be true
end
end
...@@ -1610,8 +1610,7 @@ describe Project do ...@@ -1610,8 +1610,7 @@ describe Project do
it 'imports a project' do it 'imports a project' do
expect_any_instance_of(RepositoryImportWorker).to receive(:perform).and_call_original expect_any_instance_of(RepositoryImportWorker).to receive(:perform).and_call_original
project.import_schedule expect { project.import_schedule }.to change { project.import_jid }
expect(project.reload.import_status).to eq('finished') expect(project.reload.import_status).to eq('finished')
end end
end end
...@@ -1624,6 +1623,13 @@ describe Project do ...@@ -1624,6 +1623,13 @@ describe Project do
allow(Projects::HousekeepingService).to receive(:new) { housekeeping_service } allow(Projects::HousekeepingService).to receive(:new) { housekeeping_service }
end end
it 'resets project import_error' do
error_message = 'Some error'
mirror = create(:project_empty_repo, :import_started, import_error: error_message)
expect { mirror.import_finish }.to change { mirror.import_error }.from(error_message).to(nil)
end
it 'performs housekeeping when an import of a fresh project is completed' do it 'performs housekeeping when an import of a fresh project is completed' do
project = create(:project_empty_repo, :import_started, import_type: :github) project = create(:project_empty_repo, :import_started, import_type: :github)
...@@ -1730,17 +1736,21 @@ describe Project do ...@@ -1730,17 +1736,21 @@ describe Project do
end end
describe '#add_import_job' do describe '#add_import_job' do
let(:import_jid) { '123' }
context 'forked' do context 'forked' do
let(:forked_project_link) { create(:forked_project_link, :forked_to_empty_project) } let(:forked_project_link) { create(:forked_project_link, :forked_to_empty_project) }
let(:forked_from_project) { forked_project_link.forked_from_project } let(:forked_from_project) { forked_project_link.forked_from_project }
let(:project) { forked_project_link.forked_to_project } let(:project) { forked_project_link.forked_to_project }
it 'schedules a RepositoryForkWorker job' do it 'schedules a RepositoryForkWorker job' do
expect(RepositoryForkWorker).to receive(:perform_async) expect(RepositoryForkWorker).to receive(:perform_async).with(
.with(project.id, forked_from_project.repository_storage_path, project.id,
forked_from_project.disk_path, project.namespace.full_path) forked_from_project.repository_storage_path,
forked_from_project.disk_path,
project.namespace.full_path).and_return(import_jid)
project.add_import_job expect(project.add_import_job).to eq(import_jid)
end end
end end
...@@ -1748,9 +1758,8 @@ describe Project do ...@@ -1748,9 +1758,8 @@ describe Project do
it 'schedules a RepositoryImportWorker job' do it 'schedules a RepositoryImportWorker job' do
project = create(:project, import_url: generate(:url)) project = create(:project, import_url: generate(:url))
expect(RepositoryImportWorker).to receive(:perform_async).with(project.id) expect(RepositoryImportWorker).to receive(:perform_async).with(project.id).and_return(import_jid)
expect(project.add_import_job).to eq(import_jid)
project.add_import_job
end end
end end
end end
......
...@@ -22,8 +22,8 @@ describe RepositoryImportWorker do ...@@ -22,8 +22,8 @@ describe RepositoryImportWorker do
it 'hide the credentials that were used in the import URL' do it 'hide the credentials that were used in the import URL' do
error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found } error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
project.update_attributes(import_jid: '123')
expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error }) expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error })
allow(subject).to receive(:jid).and_return('123')
expect do expect do
subject.perform(project.id) subject.perform(project.id)
......
...@@ -8,29 +8,29 @@ describe StuckImportJobsWorker do ...@@ -8,29 +8,29 @@ describe StuckImportJobsWorker do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid) allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid)
end end
describe 'long running import' do describe 'with started import_status' do
let(:project) { create(:project, import_jid: '123', import_status: 'started') } let(:project) { create(:project, :import_started, import_jid: '123') }
before do describe 'long running import' do
it 'marks the project as failed' do
allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(['123']) allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(['123'])
end
it 'marks the project as failed' do
expect { worker.perform }.to change { project.reload.import_status }.to('failed') expect { worker.perform }.to change { project.reload.import_status }.to('failed')
end end
end end
describe 'running import' do describe 'running import' do
let(:project) { create(:project, import_jid: '123', import_status: 'started') } it 'does not mark the project as failed' do
before do
allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([]) allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([])
end
it 'does not mark the project as failed' do expect { worker.perform }.not_to change { project.reload.import_status }
worker.perform end
expect(project.reload.import_status).to eq('started') describe 'import without import_jid' do
it 'marks the project as failed' do
expect { worker.perform }.to change { project.reload.import_status }.to('failed')
end
end
end end
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