Commit c09d5e17 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'fj-213436-move-update-outside-transaction' into 'master'

Bug updating snippet without repository

Closes #213436

See merge request gitlab-org/gitlab!28851
parents c6e36f95 6bd25a6c
...@@ -35,28 +35,43 @@ module Snippets ...@@ -35,28 +35,43 @@ module Snippets
private private
def save_and_commit(snippet) def save_and_commit(snippet)
snippet.with_transaction_returning_status do return false unless snippet.save
snippet.save.tap do |saved|
break false unless saved # In order to avoid non migrated snippets scenarios,
# if the snippet does not have a repository we created it
# In order to avoid non migrated snippets scenarios, # We don't need to check if the repository exists
# if the snippet does not have a repository we created it # because `create_repository` already handles it
# We don't need to check if the repository exists if Feature.enabled?(:version_snippets, current_user)
# because `create_repository` already handles it create_repository_for(snippet)
if Feature.enabled?(:version_snippets, current_user) end
create_repository_for(snippet)
end # If the snippet repository exists we commit always
# the changes
# If the snippet repository exists we commit always create_commit(snippet) if snippet.repository_exists?
# the changes
create_commit(snippet) if snippet.repository_exists? true
end rescue => e
rescue => e # Restore old attributes
snippet.errors.add(:repository, e.message) unless snippet.previous_changes.empty?
log_error(e.message) snippet.previous_changes.each { |attr, value| snippet[attr] = value[0] }
snippet.save
end
false snippet.errors.add(:repository, 'Error updating the snippet')
log_error(e.message)
# If the commit action failed we remove it because
# we don't want to leave empty repositories
# around, to allow cloning them.
if repository_empty?(snippet)
snippet.repository.remove
snippet.snippet_repository&.delete
end end
# Purge any existing value for repository_exists?
snippet.repository.expire_exists_cache
false
end end
def create_repository_for(snippet) def create_repository_for(snippet)
...@@ -81,5 +96,13 @@ module Snippets ...@@ -81,5 +96,13 @@ module Snippets
file_path: params[:file_name], file_path: params[:file_name],
content: params[:content] }] content: params[:content] }]
end end
# Because we are removing repositories we don't want to remove
# any existing repository with data. Therefore, we cannot
# rely on cached methods for that check in order to avoid losing
# data.
def repository_empty?(snippet)
snippet.repository._uncached_exists? && !snippet.repository._uncached_has_visible_content?
end
end end
end end
---
title: Fix race condition updating snippet without repository
merge_request: 28851
author:
type: fixed
...@@ -54,11 +54,9 @@ describe 'Projects > Snippets > User updates a snippet', :js do ...@@ -54,11 +54,9 @@ describe 'Projects > Snippets > User updates a snippet', :js do
end end
context 'when the git operation fails' do context 'when the git operation fails' do
let(:error_message) { 'foobar' }
before do before do
allow_next_instance_of(Snippets::UpdateService) do |instance| allow_next_instance_of(Snippets::UpdateService) do |instance|
allow(instance).to receive(:create_commit).and_raise(StandardError, error_message) allow(instance).to receive(:create_commit).and_raise(StandardError)
end end
fill_in('project_snippet_title', with: 'Snippet new title') fill_in('project_snippet_title', with: 'Snippet new title')
...@@ -67,7 +65,7 @@ describe 'Projects > Snippets > User updates a snippet', :js do ...@@ -67,7 +65,7 @@ describe 'Projects > Snippets > User updates a snippet', :js do
end end
it 'renders edit page and displays the error' do it 'renders edit page and displays the error' do
expect(page.find('.flash-container span').text).to eq(error_message) expect(page.find('.flash-container span').text).to eq('Error updating the snippet')
expect(page).to have_content('Edit Snippet') expect(page).to have_content('Edit Snippet')
end end
end end
......
...@@ -85,11 +85,9 @@ describe 'User edits snippet', :js do ...@@ -85,11 +85,9 @@ describe 'User edits snippet', :js do
end end
context 'when the git operation fails' do context 'when the git operation fails' do
let(:error_message) { 'foobar' }
before do before do
allow_next_instance_of(Snippets::UpdateService) do |instance| allow_next_instance_of(Snippets::UpdateService) do |instance|
allow(instance).to receive(:create_commit).and_raise(StandardError, error_message) allow(instance).to receive(:create_commit).and_raise(StandardError)
end end
fill_in 'personal_snippet_title', with: 'New Snippet Title' fill_in 'personal_snippet_title', with: 'New Snippet Title'
...@@ -98,7 +96,7 @@ describe 'User edits snippet', :js do ...@@ -98,7 +96,7 @@ describe 'User edits snippet', :js do
end end
it 'renders edit page and displays the error' do it 'renders edit page and displays the error' do
expect(page.find('.flash-container span').text).to eq(error_message) expect(page.find('.flash-container span').text).to eq('Error updating the snippet')
expect(page).to have_content('Edit Snippet') expect(page).to have_content('Edit Snippet')
end end
end end
......
...@@ -139,18 +139,80 @@ describe Snippets::UpdateService do ...@@ -139,18 +139,80 @@ describe Snippets::UpdateService do
subject subject
end end
end end
end
it 'returns error when the commit action fails' do shared_examples 'commit operation fails' do
error_message = 'foobar' let_it_be(:gitlab_shell) { Gitlab::Shell.new }
allow_next_instance_of(SnippetRepository) do |instance| before do
allow(instance).to receive(:multi_files_action).and_raise(SnippetRepository::CommitError, error_message) allow(service).to receive(:create_commit).and_raise(SnippetRepository::CommitError)
end end
it 'returns error' do
response = subject response = subject
expect(response).to be_error expect(response).to be_error
expect(response.payload[:snippet].errors[:repository].to_sentence).to eq error_message expect(response.payload[:snippet].errors[:repository].to_sentence).to eq 'Error updating the snippet'
end
context 'when repository is empty' do
before do
allow(service).to receive(:repository_empty?).and_return(true)
end
it 'destroys the created repository in disk' do
subject
expect(gitlab_shell.repository_exists?(snippet.repository.storage, "#{snippet.disk_path}.git")).to be_falsey
end
it 'destroys the SnippetRepository object' do
subject
expect(snippet.reload.snippet_repository).to be_nil
end
it 'expires the repository exists method cache' do
response = subject
expect(response).to be_error
expect(response.payload[:snippet].repository_exists?).to be_falsey
end
end
context 'when repository is not empty' do
before do
allow(service).to receive(:repository_empty?).and_return(false)
end
it 'does not destroy the repository' do
subject
expect(gitlab_shell.repository_exists?(snippet.repository.storage, "#{snippet.disk_path}.git")).to be_truthy
end
it 'does not destroy the snippet repository' do
subject
expect(snippet.reload.snippet_repository).not_to be_nil
end
it 'expires the repository exists method cache' do
response = subject
expect(response).to be_error
expect(response.payload[:snippet].repository_exists?).to be_truthy
end
end
it 'rolls back any snippet modifications' do
option_keys = options.stringify_keys.keys
orig_attrs = snippet.attributes.select { |k, v| k.in?(option_keys) }
subject
current_attrs = snippet.attributes.select { |k, v| k.in?(option_keys) }
expect(orig_attrs).to eq current_attrs
end end
end end
...@@ -186,12 +248,13 @@ describe Snippets::UpdateService do ...@@ -186,12 +248,13 @@ describe Snippets::UpdateService do
response = subject response = subject
expect(response).to be_error expect(response).to be_error
expect(response.payload[:snippet].errors[:repository].to_sentence).to eq error_message expect(response.payload[:snippet].errors[:repository].to_sentence).to eq 'Error updating the snippet'
end end
end end
it 'returns error if snippet does not have a snippet_repository' do it 'returns error if snippet does not have a snippet_repository' do
allow(snippet).to receive(:snippet_repository).and_return(nil) allow(snippet).to receive(:snippet_repository).and_return(nil)
allow(snippet).to receive(:track_snippet_repository).and_return(nil)
expect(subject).to be_error expect(subject).to be_error
end end
...@@ -219,11 +282,13 @@ describe Snippets::UpdateService do ...@@ -219,11 +282,13 @@ describe Snippets::UpdateService do
it_behaves_like 'public visibility level restrictions apply' it_behaves_like 'public visibility level restrictions apply'
it_behaves_like 'snippet update data is tracked' it_behaves_like 'snippet update data is tracked'
it_behaves_like 'updates repository content' it_behaves_like 'updates repository content'
it_behaves_like 'commit operation fails'
context 'when snippet does not have a repository' do context 'when snippet does not have a repository' do
let!(:snippet) { create(:project_snippet, author: user, project: project) } let!(:snippet) { create(:project_snippet, author: user, project: project) }
it_behaves_like 'creates repository and creates file' it_behaves_like 'creates repository and creates file'
it_behaves_like 'commit operation fails'
end end
end end
...@@ -235,11 +300,13 @@ describe Snippets::UpdateService do ...@@ -235,11 +300,13 @@ describe Snippets::UpdateService do
it_behaves_like 'public visibility level restrictions apply' it_behaves_like 'public visibility level restrictions apply'
it_behaves_like 'snippet update data is tracked' it_behaves_like 'snippet update data is tracked'
it_behaves_like 'updates repository content' it_behaves_like 'updates repository content'
it_behaves_like 'commit operation fails'
context 'when snippet does not have a repository' do context 'when snippet does not have a repository' do
let!(:snippet) { create(:personal_snippet, author: user, project: project) } let!(:snippet) { create(:personal_snippet, author: user, project: project) }
it_behaves_like 'creates repository and creates file' it_behaves_like 'creates repository and creates file'
it_behaves_like 'commit operation fails'
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