Update namespace statistics after personal snippet update/removal

After each update/removal of any project or personal snippet,
we need to update the project statistics (in case of project
snippets) and also the namespace root storage statistics.
parent de284cbc
...@@ -35,8 +35,8 @@ module UpdateProjectStatistics ...@@ -35,8 +35,8 @@ module UpdateProjectStatistics
@project_statistics_name = project_statistics_name @project_statistics_name = project_statistics_name
@statistic_attribute = statistic_attribute @statistic_attribute = statistic_attribute
after_save(:update_project_statistics_after_save, if: :update_project_statistics_attribute_changed?) after_save(:update_project_statistics_after_save, if: :update_project_statistics_after_save?)
after_destroy(:update_project_statistics_after_destroy, unless: :project_destroyed?) after_destroy(:update_project_statistics_after_destroy, if: :update_project_statistics_after_destroy?)
end end
private :update_project_statistics private :update_project_statistics
...@@ -45,6 +45,14 @@ module UpdateProjectStatistics ...@@ -45,6 +45,14 @@ module UpdateProjectStatistics
included do included do
private private
def update_project_statistics_after_save?
update_project_statistics_attribute_changed?
end
def update_project_statistics_after_destroy?
!project_destroyed?
end
def update_project_statistics_after_save def update_project_statistics_after_save
attr = self.class.statistic_attribute attr = self.class.statistic_attribute
delta = read_attribute(attr).to_i - attribute_before_last_save(attr).to_i delta = read_attribute(attr).to_i - attribute_before_last_save(attr).to_i
......
...@@ -12,7 +12,7 @@ class ProjectStatistics < ApplicationRecord ...@@ -12,7 +12,7 @@ class ProjectStatistics < ApplicationRecord
before_save :update_storage_size before_save :update_storage_size
COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size].freeze COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size].freeze
INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size] }.freeze INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size], snippets_size: %i[storage_size] }.freeze
NAMESPACE_RELATABLE_COLUMNS = [:repository_size, :wiki_size, :lfs_objects_size].freeze NAMESPACE_RELATABLE_COLUMNS = [:repository_size, :wiki_size, :lfs_objects_size].freeze
scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) } scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) }
......
...@@ -44,7 +44,9 @@ class Snippet < ApplicationRecord ...@@ -44,7 +44,9 @@ class Snippet < ApplicationRecord
has_many :notes, as: :noteable, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :notes, as: :noteable, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :user_mentions, class_name: "SnippetUserMention", dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :user_mentions, class_name: "SnippetUserMention", dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_one :snippet_repository, inverse_of: :snippet has_one :snippet_repository, inverse_of: :snippet
has_one :statistics, class_name: 'SnippetStatistics'
# We need to add the `dependent` in order to call the after_destroy callback
has_one :statistics, class_name: 'SnippetStatistics', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
delegate :name, :email, to: :author, prefix: true, allow_nil: true delegate :name, :email, to: :author, prefix: true, allow_nil: true
......
# frozen_string_literal: true # frozen_string_literal: true
class SnippetStatistics < ApplicationRecord class SnippetStatistics < ApplicationRecord
include AfterCommitQueue
include UpdateProjectStatistics
belongs_to :snippet belongs_to :snippet
validates :snippet, presence: true validates :snippet, presence: true
delegate :repository, to: :snippet update_project_statistics project_statistics_name: :snippets_size, statistic_attribute: :repository_size
delegate :repository, :project, :project_id, to: :snippet
after_save :update_author_root_storage_statistics, if: :update_author_root_storage_statistics?
after_destroy :update_author_root_storage_statistics, unless: :project_snippet?
def update_commit_count def update_commit_count
self.commit_count = repository.commit_count self.commit_count = repository.commit_count
...@@ -32,4 +40,30 @@ class SnippetStatistics < ApplicationRecord ...@@ -32,4 +40,30 @@ class SnippetStatistics < ApplicationRecord
save! save!
end end
private
alias_method :original_update_project_statistics_after_save?, :update_project_statistics_after_save?
def update_project_statistics_after_save?
project_snippet? && original_update_project_statistics_after_save?
end
alias_method :original_update_project_statistics_after_destroy?, :update_project_statistics_after_destroy?
def update_project_statistics_after_destroy?
project_snippet? && original_update_project_statistics_after_destroy?
end
def update_author_root_storage_statistics?
!project_snippet? && saved_change_to_repository_size?
end
def update_author_root_storage_statistics
run_after_commit do
Namespaces::ScheduleAggregationWorker.perform_async(snippet.author.namespace_id)
end
end
def project_snippet?
snippet.is_a?(ProjectSnippet)
end
end end
...@@ -27,11 +27,6 @@ module Snippets ...@@ -27,11 +27,6 @@ module Snippets
attempt_destroy! attempt_destroy!
# Update project statistics if the snippet is a Project one
if snippet.project_id
ProjectCacheWorker.perform_async(snippet.project_id, [], [:snippets_size])
end
ServiceResponse.success(message: 'Snippet was deleted.') ServiceResponse.success(message: 'Snippet was deleted.')
rescue DestroyError rescue DestroyError
service_response_error('Failed to remove snippet repository.', 400) service_response_error('Failed to remove snippet repository.', 400)
......
...@@ -16,11 +16,6 @@ module Snippets ...@@ -16,11 +16,6 @@ module Snippets
snippet.repository.expire_statistics_caches snippet.repository.expire_statistics_caches
statistics.refresh! statistics.refresh!
# Update project statistics if the snippet is a Project one
if snippet.project_id
ProjectCacheWorker.perform_async(snippet.project_id, [], [:snippets_size])
end
ServiceResponse.success(message: 'Snippet statistics successfully updated.') ServiceResponse.success(message: 'Snippet statistics successfully updated.')
end end
......
---
title: Update namespace statistics after personal snippet update/removal
merge_request: 36031
author:
type: changed
# frozen_string_literal: true
FactoryBot.define do
factory :snippet_statistics do
snippet
initialize_with do
# statistics are automatically created when a snippet is created
snippet&.statistics || new
end
transient do
with_data { false }
size_multiplier { 1 }
end
after(:build) do |snippet_statistics, evaluator|
if evaluator.with_data
snippet_statistics.repository_size = evaluator.size_multiplier
snippet_statistics.commit_count = evaluator.size_multiplier * 2
snippet_statistics.file_count = evaluator.size_multiplier * 3
end
end
end
end
...@@ -20,7 +20,7 @@ RSpec.describe Snippet do ...@@ -20,7 +20,7 @@ RSpec.describe Snippet do
it { is_expected.to have_many(:award_emoji).dependent(:destroy) } it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
it { is_expected.to have_many(:user_mentions).class_name("SnippetUserMention") } it { is_expected.to have_many(:user_mentions).class_name("SnippetUserMention") }
it { is_expected.to have_one(:snippet_repository) } it { is_expected.to have_one(:snippet_repository) }
it { is_expected.to have_one(:statistics).class_name('SnippetStatistics') } it { is_expected.to have_one(:statistics).class_name('SnippetStatistics').dependent(:destroy) }
end end
describe 'validation' do describe 'validation' do
......
...@@ -86,4 +86,64 @@ RSpec.describe SnippetStatistics do ...@@ -86,4 +86,64 @@ RSpec.describe SnippetStatistics do
subject subject
end end
end end
context 'with a PersonalSnippet' do
let!(:snippet) { create(:personal_snippet, :repository) }
shared_examples 'personal snippet statistics updates' do
it 'schedules a namespace statistics worker' do
expect(Namespaces::ScheduleAggregationWorker)
.to receive(:perform_async).once
statistics.save!
end
it 'does not try to update project stats' do
expect(statistics).not_to receive(:schedule_update_project_statistic)
statistics.save!
end
end
context 'when creating' do
let(:statistics) { build(:snippet_statistics, snippet_id: snippet.id, with_data: true) }
before do
snippet.statistics.delete
end
it_behaves_like 'personal snippet statistics updates'
end
context 'when updating' do
let(:statistics) { snippet.statistics }
before do
snippet.statistics.repository_size = 123
end
it_behaves_like 'personal snippet statistics updates'
end
end
context 'with a ProjectSnippet' do
let!(:snippet) { create(:project_snippet) }
it_behaves_like 'UpdateProjectStatistics' do
subject { build(:snippet_statistics, snippet: snippet, id: snippet.id, with_data: true) }
before do
# The shared examples requires the snippet statistics not to be present
snippet.statistics.delete
snippet.reload
end
end
it 'does not call personal snippet callbacks' do
expect(snippet.statistics).not_to receive(:update_author_root_storage_statistics)
expect(snippet.statistics).to receive(:schedule_update_project_statistic)
snippet.statistics.update!(repository_size: 123)
end
end
end end
...@@ -106,11 +106,24 @@ RSpec.describe Snippets::DestroyService do ...@@ -106,11 +106,24 @@ RSpec.describe Snippets::DestroyService do
it_behaves_like 'a successful destroy' it_behaves_like 'a successful destroy'
it_behaves_like 'deletes the snippet repository' it_behaves_like 'deletes the snippet repository'
it 'schedules a project cache update for snippet_size' do context 'project statistics' do
expect(ProjectCacheWorker).to receive(:perform_async) before do
.with(snippet.project_id, [], [:snippets_size]) snippet.statistics.refresh!
end
subject it 'updates stats after deletion' do
expect(project.reload.statistics.snippets_size).not_to be_zero
subject
expect(project.reload.statistics.snippets_size).to be_zero
end
it 'schedules a namespace statistics update' do
expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async).with(project.namespace_id).once
subject
end
end end
end end
...@@ -130,8 +143,8 @@ RSpec.describe Snippets::DestroyService do ...@@ -130,8 +143,8 @@ RSpec.describe Snippets::DestroyService do
it_behaves_like 'a successful destroy' it_behaves_like 'a successful destroy'
it_behaves_like 'deletes the snippet repository' it_behaves_like 'deletes the snippet repository'
it 'does not schedule a project cache update' do it 'schedules a namespace statistics update' do
expect(ProjectCacheWorker).not_to receive(:perform_async) expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async).with(author.namespace_id)
subject subject
end end
......
...@@ -17,17 +17,6 @@ RSpec.describe Snippets::UpdateStatisticsService do ...@@ -17,17 +17,6 @@ RSpec.describe Snippets::UpdateStatisticsService do
subject subject
end end
it 'schedules project cache worker based on type' do
if snippet.project_id
expect(ProjectCacheWorker).to receive(:perform_async)
.with(snippet.project_id, [], [:snippets_size])
else
expect(ProjectCacheWorker).not_to receive(:perform_async)
end
subject
end
context 'when snippet statistics does not exist' do context 'when snippet statistics does not exist' do
it 'creates snippet statistics' do it 'creates snippet statistics' do
snippet.statistics.delete snippet.statistics.delete
...@@ -64,6 +53,13 @@ RSpec.describe Snippets::UpdateStatisticsService do ...@@ -64,6 +53,13 @@ RSpec.describe Snippets::UpdateStatisticsService do
expect(subject).to be_error expect(subject).to be_error
end end
end end
it 'schedules a namespace storage statistics update' do
expect(Namespaces::ScheduleAggregationWorker)
.to receive(:perform_async).once
subject
end
end end
context 'with PersonalSnippet' do context 'with PersonalSnippet' do
...@@ -74,8 +70,17 @@ RSpec.describe Snippets::UpdateStatisticsService do ...@@ -74,8 +70,17 @@ RSpec.describe Snippets::UpdateStatisticsService do
context 'with ProjectSnippet' do context 'with ProjectSnippet' do
let!(:snippet) { create(:project_snippet, :repository) } let!(:snippet) { create(:project_snippet, :repository) }
let(:project_statistics) { snippet.project.statistics }
it_behaves_like 'updates statistics' it_behaves_like 'updates statistics'
it 'updates projects statistics "snippets_size"' do
expect(project_statistics.snippets_size).to be_zero
subject
expect(snippet.reload.statistics.repository_size).to eq project_statistics.reload.snippets_size
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