Commit e48ba301 authored by Patrick Bajao's avatar Patrick Bajao

Handle cache busting scenario when note author detail changes

Before when a note author which is part of a discussion has its
detail updated (e.g. name change, username change), the discussion
cache wont be busted.

To ensure that we show updated information when that happens, the
author's cache key is now included in
`Note#post_processed_cache_key` which we call in
`Discussion#cache_key`.

This way, when the author is updated, the discussion cache will be
invalidated as well.
parent ef7cb02f
...@@ -587,7 +587,7 @@ class Note < ApplicationRecord ...@@ -587,7 +587,7 @@ class Note < ApplicationRecord
end end
def post_processed_cache_key def post_processed_cache_key
cache_key_items = [cache_key] cache_key_items = [cache_key, author.cache_key]
cache_key_items << Digest::SHA1.hexdigest(redacted_note_html) if redacted_note_html.present? cache_key_items << Digest::SHA1.hexdigest(redacted_note_html) if redacted_note_html.present?
cache_key_items.join(':') cache_key_items.join(':')
......
...@@ -28,11 +28,12 @@ module Users ...@@ -28,11 +28,12 @@ module Users
params[:emoji] = UserStatus::DEFAULT_EMOJI if params[:emoji].blank? params[:emoji] = UserStatus::DEFAULT_EMOJI if params[:emoji].blank?
params[:availability] = UserStatus.availabilities[:not_set] unless new_user_availability params[:availability] = UserStatus.availabilities[:not_set] unless new_user_availability
user_status.update(params) bump_user if user_status.update(params)
end end
def remove_status def remove_status
UserStatus.delete(target_user.id) bump_user if UserStatus.delete(target_user.id).nonzero?
true
end end
def user_status def user_status
...@@ -48,5 +49,12 @@ module Users ...@@ -48,5 +49,12 @@ module Users
def new_user_availability def new_user_availability
UserStatus.availabilities[params[:availability]] UserStatus.availabilities[params[:availability]]
end end
def bump_user
# Intentionally not calling `touch` as that will trigger other callbacks
# on target_user (e.g. after_touch, after_commit, after_rollback) and we
# don't need them to happen here.
target_user.update_column(:updated_at, Time.current)
end
end end
end end
...@@ -1542,8 +1542,8 @@ RSpec.describe Note do ...@@ -1542,8 +1542,8 @@ RSpec.describe Note do
describe '#post_processed_cache_key' do describe '#post_processed_cache_key' do
let(:note) { build(:note) } let(:note) { build(:note) }
it 'returns cache key by default' do it 'returns cache key and author cache key by default' do
expect(note.post_processed_cache_key).to eq(note.cache_key) expect(note.post_processed_cache_key).to eq("#{note.cache_key}:#{note.author.cache_key}")
end end
context 'when note has redacted_note_html' do context 'when note has redacted_note_html' do
...@@ -1554,7 +1554,7 @@ RSpec.describe Note do ...@@ -1554,7 +1554,7 @@ RSpec.describe Note do
end end
it 'returns cache key with redacted_note_html sha' do it 'returns cache key with redacted_note_html sha' do
expect(note.post_processed_cache_key).to eq("#{note.cache_key}:#{Digest::SHA1.hexdigest(redacted_note_html)}") expect(note.post_processed_cache_key).to eq("#{note.cache_key}:#{note.author.cache_key}:#{Digest::SHA1.hexdigest(redacted_note_html)}")
end end
end end
end end
......
...@@ -55,7 +55,8 @@ RSpec.describe 'merge requests discussions' do ...@@ -55,7 +55,8 @@ RSpec.describe 'merge requests discussions' do
context 'caching', :use_clean_rails_memory_store_caching do context 'caching', :use_clean_rails_memory_store_caching do
let(:reference) { create(:issue, project: project) } let(:reference) { create(:issue, project: project) }
let!(:first_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, note: "reference: #{reference.to_reference}") } let(:author) { create(:user) }
let!(:first_note) { create(:diff_note_on_merge_request, author: author, noteable: merge_request, project: project, note: "reference: #{reference.to_reference}") }
let!(:second_note) { create(:diff_note_on_merge_request, in_reply_to: first_note, noteable: merge_request, project: project) } let!(:second_note) { create(:diff_note_on_merge_request, in_reply_to: first_note, noteable: merge_request, project: project) }
let!(:award_emoji) { create(:award_emoji, awardable: first_note) } let!(:award_emoji) { create(:award_emoji, awardable: first_note) }
...@@ -181,6 +182,26 @@ RSpec.describe 'merge requests discussions' do ...@@ -181,6 +182,26 @@ RSpec.describe 'merge requests discussions' do
end end
end end
context 'when author detail changes' do
before do
author.update!(name: "#{author.name} (Updated)")
end
it_behaves_like 'cache miss' do
let(:changed_notes) { [first_note, second_note] }
end
end
context 'when author status changes' do
before do
Users::SetStatusService.new(author, message: "updated status").execute
end
it_behaves_like 'cache miss' do
let(:changed_notes) { [first_note, second_note] }
end
end
context 'when merge_request_discussion_cache is disabled' do context 'when merge_request_discussion_cache is disabled' do
before do before do
stub_feature_flags(merge_request_discussion_cache: false) stub_feature_flags(merge_request_discussion_cache: false)
......
...@@ -8,6 +8,18 @@ RSpec.describe Users::SetStatusService do ...@@ -8,6 +8,18 @@ RSpec.describe Users::SetStatusService do
subject(:service) { described_class.new(current_user, params) } subject(:service) { described_class.new(current_user, params) }
describe '#execute' do describe '#execute' do
shared_examples_for 'bumps user' do
it 'bumps User#updated_at' do
expect { service.execute }.to change { current_user.updated_at }
end
end
shared_examples_for 'does not bump user' do
it 'does not bump User#updated_at' do
expect { service.execute }.not_to change { current_user.updated_at }
end
end
context 'when params are set' do context 'when params are set' do
let(:params) { { emoji: 'taurus', message: 'a random status', availability: 'busy' } } let(:params) { { emoji: 'taurus', message: 'a random status', availability: 'busy' } }
...@@ -31,6 +43,8 @@ RSpec.describe Users::SetStatusService do ...@@ -31,6 +43,8 @@ RSpec.describe Users::SetStatusService do
expect(service.execute).to be(true) expect(service.execute).to be(true)
end end
it_behaves_like 'bumps user'
context 'when setting availability to not_set' do context 'when setting availability to not_set' do
before do before do
params[:availability] = 'not_set' params[:availability] = 'not_set'
...@@ -72,6 +86,8 @@ RSpec.describe Users::SetStatusService do ...@@ -72,6 +86,8 @@ RSpec.describe Users::SetStatusService do
it 'does not update the status if the current user is not allowed' do it 'does not update the status if the current user is not allowed' do
expect { service.execute }.not_to change { target_user.status } expect { service.execute }.not_to change { target_user.status }
end end
it_behaves_like 'does not bump user'
end end
end end
...@@ -79,20 +95,28 @@ RSpec.describe Users::SetStatusService do ...@@ -79,20 +95,28 @@ RSpec.describe Users::SetStatusService do
let(:params) { {} } let(:params) { {} }
shared_examples 'removes user status record' do shared_examples 'removes user status record' do
it 'deletes the status' do it 'deletes the user status record' do
status = create(:user_status, user: current_user)
expect { service.execute } expect { service.execute }
.to change { current_user.reload.status }.from(status).to(nil) .to change { current_user.reload.status }.from(user_status).to(nil)
end end
end
it_behaves_like 'removes user status record' it_behaves_like 'bumps user'
end
context 'when not_set is given for availability' do context 'when user has existing user status record' do
let(:params) { { availability: 'not_set' } } let!(:user_status) { create(:user_status, user: current_user) }
it_behaves_like 'removes user status record' it_behaves_like 'removes user status record'
context 'when not_set is given for availability' do
let(:params) { { availability: 'not_set' } }
it_behaves_like 'removes user status record'
end
end
context 'when user has no existing user status record' do
it_behaves_like 'does not bump user'
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