Commit 08d6b67f authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch '220104-fix-race-in-read-only-repository-logic' into 'master'

Fix a race condition checking whether a project is read-only

See merge request gitlab-org/gitlab!45160
parents c1f022e7 43f9b983
...@@ -2090,21 +2090,36 @@ class Project < ApplicationRecord ...@@ -2090,21 +2090,36 @@ class Project < ApplicationRecord
(auto_devops || build_auto_devops)&.predefined_variables (auto_devops || build_auto_devops)&.predefined_variables
end end
# Tries to set repository as read_only, checking for existing Git transfers in progress beforehand RepositoryReadOnlyError = Class.new(StandardError)
# Tries to set repository as read_only, checking for existing Git transfers in
# progress beforehand. Setting a repository read-only will fail if it is
# already in that state.
# #
# @return [Boolean] true when set to read_only or false when an existing git transfer is in progress # @return nil. Failures will raise an exception
def set_repository_read_only! def set_repository_read_only!
with_lock do with_lock do
break false if git_transfer_in_progress? raise RepositoryReadOnlyError, _('Git transfer in progress') if
git_transfer_in_progress?
raise RepositoryReadOnlyError, _('Repository already read-only') if
self.class.where(id: id).pick(:repository_read_only)
raise ActiveRecord::RecordNotSaved, _('Database update failed') unless
update_column(:repository_read_only, true)
update_column(:repository_read_only, true) nil
end end
end end
# Set repository as writable again # Set repository as writable again. Unlike setting it read-only, this will
# succeed if the repository is already writable.
def set_repository_writable! def set_repository_writable!
with_lock do with_lock do
update_column(:repository_read_only, false) raise ActiveRecord::RecordNotSaved, _('Database update failed') unless
update_column(:repository_read_only, false)
nil
end end
end end
......
...@@ -46,8 +46,15 @@ class ProjectRepositoryStorageMove < ApplicationRecord ...@@ -46,8 +46,15 @@ class ProjectRepositoryStorageMove < ApplicationRecord
transition replicated: :cleanup_failed transition replicated: :cleanup_failed
end end
after_transition initial: :scheduled do |storage_move| around_transition initial: :scheduled do |storage_move, block|
storage_move.project.update_column(:repository_read_only, true) block.call
begin
storage_move.project.set_repository_read_only!
rescue => err
errors.add(:project, err.message)
next false
end
storage_move.run_after_commit do storage_move.run_after_commit do
ProjectUpdateRepositoryStorageWorker.perform_async( ProjectUpdateRepositoryStorageWorker.perform_async(
...@@ -56,17 +63,18 @@ class ProjectRepositoryStorageMove < ApplicationRecord ...@@ -56,17 +63,18 @@ class ProjectRepositoryStorageMove < ApplicationRecord
storage_move.id storage_move.id
) )
end end
true
end end
after_transition started: :replicated do |storage_move| before_transition started: :replicated do |storage_move|
storage_move.project.update_columns( storage_move.project.set_repository_writable!
repository_read_only: false,
repository_storage: storage_move.destination_storage_name storage_move.project.update_column(:repository_storage, storage_move.destination_storage_name)
)
end end
after_transition started: :failed do |storage_move| before_transition started: :failed do |storage_move|
storage_move.project.update_column(:repository_read_only, false) storage_move.project.set_repository_writable!
end end
state :initial, value: 1 state :initial, value: 1
......
...@@ -79,13 +79,12 @@ module Projects ...@@ -79,13 +79,12 @@ module Projects
end end
def try_to_set_repository_read_only! def try_to_set_repository_read_only!
# Mitigate any push operation to start during migration project.set_repository_read_only!
unless project.set_repository_read_only! rescue Project::RepositoryReadOnlyError => err
migration_error = "Target repository '#{old_disk_path}' cannot be made read-only as there is a git transfer in progress" migration_error = "Target repository '#{old_disk_path}' cannot be made read-only: #{err.message}"
logger.error migration_error logger.error migration_error
raise RepositoryInUseError, migration_error raise RepositoryInUseError, migration_error
end
end end
def wiki_path_suffix def wiki_path_suffix
......
...@@ -21,8 +21,10 @@ module Projects ...@@ -21,8 +21,10 @@ module Projects
project.storage_version = nil project.storage_version = nil
end end
project.repository_read_only = false project.transaction do
project.save!(validate: false) project.save!(validate: false)
project.set_repository_writable!
end
if result && block_given? if result && block_given?
yield yield
......
---
title: Fix a race condition checking whether a project is read-only
merge_request: 45160
author:
type: fixed
...@@ -8411,6 +8411,9 @@ msgstr "" ...@@ -8411,6 +8411,9 @@ msgstr ""
msgid "Data is still calculating..." msgid "Data is still calculating..."
msgstr "" msgstr ""
msgid "Database update failed"
msgstr ""
msgid "Datasource name not found" msgid "Datasource name not found"
msgstr "" msgstr ""
...@@ -12249,6 +12252,9 @@ msgstr "" ...@@ -12249,6 +12252,9 @@ msgstr ""
msgid "Git strategy for pipelines" msgid "Git strategy for pipelines"
msgstr "" msgstr ""
msgid "Git transfer in progress"
msgstr ""
msgid "Git version" msgid "Git version"
msgstr "" msgstr ""
...@@ -22297,6 +22303,9 @@ msgstr "" ...@@ -22297,6 +22303,9 @@ msgstr ""
msgid "Repository Settings" msgid "Repository Settings"
msgstr "" msgstr ""
msgid "Repository already read-only"
msgstr ""
msgid "Repository check" msgid "Repository check"
msgstr "" msgstr ""
......
...@@ -3003,14 +3003,23 @@ RSpec.describe Project do ...@@ -3003,14 +3003,23 @@ RSpec.describe Project do
describe '#set_repository_read_only!' do describe '#set_repository_read_only!' do
let(:project) { create(:project) } let(:project) { create(:project) }
it 'returns true when there is no existing git transfer in progress' do it 'makes the repository read-only' do
expect(project.set_repository_read_only!).to be_truthy expect { project.set_repository_read_only! }
.to change(project, :repository_read_only?)
.from(false)
.to(true)
end end
it 'returns false when there is an existing git transfer in progress' do it 'raises an error if the project is already read-only' do
project.set_repository_read_only!
expect { project.set_repository_read_only! }.to raise_error(described_class::RepositoryReadOnlyError, /already read-only/)
end
it 'raises an error when there is an existing git transfer in progress' do
allow(project).to receive(:git_transfer_in_progress?) { true } allow(project).to receive(:git_transfer_in_progress?) { true }
expect(project.set_repository_read_only!).to be_falsey expect { project.set_repository_read_only! }.to raise_error(described_class::RepositoryReadOnlyError, /in progress/)
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