Defer project destroys within a namespace in Groups::DestroyService#async_execute

Group#destroy would actually hard-delete all associated projects even
though the acts_as_paranoia gem is used, preventing Projects::DestroyService
from doing any work.

We first noticed this while trying to log all projects deletion to the Geo
log.
This commit is contained in:
Stan Hu 2017-06-23 17:02:33 -07:00
parent 5a983ac431
commit fa93156528
5 changed files with 54 additions and 22 deletions

View file

@ -219,6 +219,12 @@ class Namespace < ActiveRecord::Base
parent.present?
end
def soft_delete_without_removing_associations
# We can't use paranoia's `#destroy` since this will hard-delete projects.
# Project uses `pending_delete` instead of the acts_as_paranoia gem.
self.deleted_at = Time.now
end
private
def repository_storage_paths

View file

@ -1,8 +1,7 @@
module Groups
class DestroyService < Groups::BaseService
def async_execute
# Soft delete via paranoia gem
group.destroy
group.soft_delete_without_removing_associations
job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
end

View file

@ -0,0 +1,4 @@
---
title: Defer project destroys within a namespace in Groups::DestroyService#async_execute
merge_request:
author:

View file

@ -342,6 +342,17 @@ describe Namespace, models: true do
end
end
describe '#soft_delete_without_removing_associations' do
let(:project1) { create(:project_empty_repo, namespace: namespace) }
it 'updates the deleted_at timestamp but preserves projects' do
namespace.soft_delete_without_removing_associations
expect(Project.all).to include(project1)
expect(namespace.deleted_at).not_to be_nil
end
end
describe '#user_ids_for_project_authorizations' do
it 'returns the user IDs for which to refresh authorizations' do
expect(namespace.user_ids_for_project_authorizations)

View file

@ -15,6 +15,14 @@ describe Groups::DestroyService, services: true do
group.add_user(user, Gitlab::Access::OWNER)
end
def destroy_group(group, user, async)
if async
Groups::DestroyService.new(group, user).async_execute
else
Groups::DestroyService.new(group, user).execute
end
end
shared_examples 'group destruction' do |async|
context 'database records' do
before do
@ -30,30 +38,14 @@ describe Groups::DestroyService, services: true do
context 'file system' do
context 'Sidekiq inline' do
before do
# Run sidekiq immediatly to check that renamed dir will be removed
# Run sidekiq immediately to check that renamed dir will be removed
Sidekiq::Testing.inline! { destroy_group(group, user, async) }
end
it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey }
end
context 'Sidekiq fake' do
before do
# Don't run sidekiq to check if renamed repository exists
Sidekiq::Testing.fake! { destroy_group(group, user, async) }
it 'verifies that paths have been deleted' do
expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey
expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
end
it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy }
end
end
def destroy_group(group, user, async)
if async
Groups::DestroyService.new(group, user).async_execute
else
Groups::DestroyService.new(group, user).execute
end
end
end
@ -61,6 +53,26 @@ describe Groups::DestroyService, services: true do
describe 'asynchronous delete' do
it_behaves_like 'group destruction', true
context 'Sidekiq fake' do
before do
# Don't run Sidekiq to verify that group and projects are not actually destroyed
Sidekiq::Testing.fake! { destroy_group(group, user, true) }
end
after do
# Clean up stale directories
gitlab_shell.rm_namespace(project.repository_storage_path, group.path)
gitlab_shell.rm_namespace(project.repository_storage_path, remove_path)
end
it 'verifies original paths and projects still exist' do
expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_truthy
expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
expect(Project.unscoped.count).to eq(1)
expect(Group.unscoped.count).to eq(2)
end
end
context 'potential race conditions' do
context "when the `GroupDestroyWorker` task runs immediately" do
it "deletes the group" do