Skip to content
Snippets Groups Projects
Commit 96ad24a8 authored by Douwe Maan's avatar Douwe Maan Committed by 🤖 GitLab Bot 🤖
Browse files

Merge branch 'banzai-avoid-redis-if-db-cache' into 'master'

Banzai - avoid redis if attr is in DB cache

See merge request gitlab-org/gitlab-ce!30334

(cherry picked from commit 92fac459)

e5705f5c Banzai - avoid redis if attr is in DB cache
parent 2c398bd8
No related branches found
No related tags found
No related merge requests found
Showing with 194 additions and 48 deletions
Loading
Loading
@@ -87,6 +87,16 @@ module CacheMarkdownField
__send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend
end
 
# Updates the markdown cache if necessary, then returns the field
# Unlike `cached_html_for` it returns `nil` if the field does not exist
def updated_cached_html_for(markdown_field)
return unless cached_markdown_fields.markdown_fields.include?(markdown_field)
refresh_markdown_cache if attribute_invalidated?(cached_markdown_fields.html_field(markdown_field))
cached_html_for(markdown_field)
end
def latest_cached_markdown_version
@latest_cached_markdown_version ||= (Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16) | local_version
end
Loading
Loading
@@ -139,8 +149,9 @@ module CacheMarkdownField
# The HTML becomes invalid if any dependent fields change. For now, assume
# author and project invalidate the cache in all circumstances.
define_method(invalidation_method) do
invalidations = changed_markdown_fields & [markdown_field.to_s, *INVALIDATED_BY]
invalidations.delete(markdown_field.to_s) if changed_markdown_fields.include?("#{markdown_field}_html")
changed_fields = changed_attributes.keys
invalidations = changed_fields & [markdown_field.to_s, *INVALIDATED_BY]
invalidations.delete(markdown_field.to_s) if changed_fields.include?("#{markdown_field}_html")
!invalidations.empty? || !cached_html_up_to_date?(markdown_field)
end
end
Loading
Loading
Loading
Loading
@@ -63,6 +63,9 @@ module Mentionable
skip_project_check: skip_project_check?
).merge(mentionable_params)
 
cached_html = self.try(:updated_cached_html_for, attr.to_sym)
options[:rendered] = cached_html if cached_html
extractor.analyze(text, options)
end
 
Loading
Loading
Loading
Loading
@@ -55,11 +55,16 @@ module Banzai
# Perform multiple render from an Array of Markdown String into an
# Array of HTML-safe String of HTML.
#
# As the rendered Markdown String can be already cached read all the data
# from the cache using Rails.cache.read_multi operation. If the Markdown String
# is not in the cache or it's not cacheable (no cache_key entry is provided in
# the context) the Markdown String is rendered and stored in the cache so the
# next render call gets the rendered HTML-safe String from the cache.
# The redis cache is completely obviated if we receive a `:rendered` key in the
# context, as it is assumed the item has been pre-rendered somewhere else and there
# is no need to cache it.
#
# If no `:rendered` key is present in the context, as the rendered Markdown String
# can be already cached, read all the data from the cache using
# Rails.cache.read_multi operation. If the Markdown String is not in the cache
# or it's not cacheable (no cache_key entry is provided in the context) the
# Markdown String is rendered and stored in the cache so the next render call
# gets the rendered HTML-safe String from the cache.
#
# For further explanation see #render method comments.
#
Loading
Loading
@@ -76,19 +81,34 @@ module Banzai
# => [{ text: '### Hello',
# context: { cache_key: [note, :note] } }]
def self.cache_collection_render(texts_and_contexts)
items_collection = texts_and_contexts.each_with_index do |item, index|
items_collection = texts_and_contexts.each do |item|
context = item[:context]
cache_key = full_cache_multi_key(context.delete(:cache_key), context[:pipeline])
 
item[:cache_key] = cache_key if cache_key
if context.key?(:rendered)
item[:rendered] = context.delete(:rendered)
else
# If the attribute didn't come in pre-rendered, let's prepare it for caching it in redis
cache_key = full_cache_multi_key(context.delete(:cache_key), context[:pipeline])
item[:cache_key] = cache_key if cache_key
end
end
 
cacheable_items, non_cacheable_items = items_collection.partition { |item| item.key?(:cache_key) }
cacheable_items, non_cacheable_items = items_collection.group_by do |item|
if item.key?(:rendered)
# We're not really doing anything here as these don't need any processing, but leaving it just in case
# as they could have a cache_key and we don't want them to be re-rendered
:rendered
elsif item.key?(:cache_key)
:cacheable
else
:non_cacheable
end
end.values_at(:cacheable, :non_cacheable)
 
items_in_cache = []
items_not_in_cache = []
 
unless cacheable_items.empty?
if cacheable_items.present?
items_in_cache = Rails.cache.read_multi(*cacheable_items.map { |item| item[:cache_key] })
items_not_in_cache = cacheable_items.reject do |item|
item[:rendered] = items_in_cache[item[:cache_key]]
Loading
Loading
@@ -96,7 +116,7 @@ module Banzai
end
end
 
(items_not_in_cache + non_cacheable_items).each do |item|
(items_not_in_cache + Array.wrap(non_cacheable_items)).each do |item|
item[:rendered] = render(item[:text], item[:context])
Rails.cache.write(item[:cache_key], item[:rendered]) if item[:cache_key]
end
Loading
Loading
Loading
Loading
@@ -26,10 +26,6 @@ module Gitlab
attrs
end
 
def changed_markdown_fields
changed_attributes.keys.map(&:to_s) & cached_markdown_fields.markdown_fields.map(&:to_s)
end
def write_markdown_field(field_name, value)
write_attribute(field_name, value)
end
Loading
Loading
Loading
Loading
@@ -36,8 +36,8 @@ module Gitlab
false
end
 
def changed_markdown_fields
[]
def changed_attributes
{}
end
 
def cached_markdown
Loading
Loading
Loading
Loading
@@ -19,6 +19,24 @@ describe Banzai::Renderer do
object
end
 
describe '#cache_collection_render' do
let(:merge_request) { fake_object(fresh: true) }
let(:context) { { cache_key: [merge_request, 'field'], rendered: merge_request.field_html } }
context 'when an item has a rendered field' do
before do
allow(merge_request).to receive(:field).and_return('This is the field')
allow(merge_request).to receive(:field_html).and_return('This is the field')
end
it 'does not touch redis if the field is in the cache' do
expect(Rails).not_to receive(:cache)
described_class.cache_collection_render([{ text: merge_request.field, context: context }])
end
end
end
describe '#render_field' do
let(:renderer) { described_class }
 
Loading
Loading
Loading
Loading
@@ -9,12 +9,13 @@ describe Gitlab::MarkdownCache::ActiveRecord::Extension do
cache_markdown_field :title, whitelisted: true
cache_markdown_field :description, pipeline: :single_line
 
attr_accessor :author, :project
attribute :author
attribute :project
end
end
 
let(:cache_version) { Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16 }
let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) }
let(:thing) { klass.create(title: markdown, title_html: html, cached_markdown_version: cache_version) }
 
let(:markdown) { '`Foo`' }
let(:html) { '<p data-sourcepos="1:1-1:5" dir="auto"><code>Foo</code></p>' }
Loading
Loading
@@ -37,7 +38,7 @@ describe Gitlab::MarkdownCache::ActiveRecord::Extension do
end
 
context 'a changed markdown field' do
let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) }
let(:thing) { klass.create(title: markdown, title_html: html, cached_markdown_version: cache_version) }
 
before do
thing.title = updated_markdown
Loading
Loading
Loading
Loading
@@ -139,8 +139,8 @@ describe CommitRange do
end
 
describe '#has_been_reverted?' do
let(:issue) { create(:issue) }
let(:user) { issue.author }
let(:user) { create(:user) }
let(:issue) { create(:issue, author: user, project: project) }
 
it 'returns true if the commit has been reverted' do
create(:note_on_issue,
Loading
Loading
@@ -149,9 +149,11 @@ describe CommitRange do
note: commit1.revert_description(user),
project: issue.project)
 
expect_any_instance_of(Commit).to receive(:reverts_commit?)
.with(commit1, user)
.and_return(true)
expect_next_instance_of(Commit) do |commit|
expect(commit).to receive(:reverts_commit?)
.with(commit1, user)
.and_return(true)
end
 
expect(commit1.has_been_reverted?(user, issue.notes_with_associations)).to eq(true)
end
Loading
Loading
Loading
Loading
@@ -198,6 +198,36 @@ describe CacheMarkdownField, :clean_gitlab_redis_cache do
end
end
end
describe '#updated_cached_html_for' do
let(:thing) { klass.new(description: markdown, description_html: html, cached_markdown_version: cache_version) }
context 'when the markdown cache is outdated' do
before do
thing.cached_markdown_version += 1
end
it 'calls #refresh_markdown_cache' do
expect(thing).to receive(:refresh_markdown_cache)
expect(thing.updated_cached_html_for(:description)).to eq(html)
end
end
context 'when the markdown field does not exist' do
it 'returns nil' do
expect(thing.updated_cached_html_for(:something)).to eq(nil)
end
end
context 'when the markdown cache is up to date' do
it 'does not call #refresh_markdown_cache' do
expect(thing).not_to receive(:refresh_markdown_cache)
expect(thing.updated_cached_html_for(:description)).to eq(html)
end
end
end
end
 
context 'for Active record classes' do
Loading
Loading
Loading
Loading
@@ -177,6 +177,7 @@ describe Note do
pipeline: :note,
cache_key: [note1, "note"],
project: note1.project,
rendered: note1.note_html,
author: note1.author
}
}]).and_call_original
Loading
Loading
@@ -189,6 +190,7 @@ describe Note do
pipeline: :note,
cache_key: [note2, "note"],
project: note2.project,
rendered: note2.note_html,
author: note2.author
}
}]).and_call_original
Loading
Loading
Loading
Loading
@@ -215,13 +215,14 @@ describe NotificationService, :mailer do
let(:project) { create(:project, :private) }
let(:issue) { create(:issue, project: project, assignees: [assignee]) }
let(:mentioned_issue) { create(:issue, assignees: issue.assignees) }
let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@mention referenced, @unsubscribed_mentioned and @outsider also') }
let(:author) { create(:user) }
let(:note) { create(:note_on_issue, author: author, noteable: issue, project_id: issue.project_id, note: '@mention referenced, @unsubscribed_mentioned and @outsider also') }
 
before do
build_team(note.project)
build_team(project)
project.add_maintainer(issue.author)
project.add_maintainer(assignee)
project.add_maintainer(note.author)
project.add_maintainer(author)
 
@u_custom_off = create_user_with_notification(:custom, 'custom_off')
project.add_guest(@u_custom_off)
Loading
Loading
@@ -240,7 +241,8 @@ describe NotificationService, :mailer do
 
describe '#new_note' do
it do
add_users_with_subscription(note.project, issue)
add_users(project)
add_user_subscriptions(issue)
reset_delivered_emails!
 
expect(SentNotification).to receive(:record).with(issue, any_args).exactly(10).times
Loading
Loading
@@ -268,7 +270,8 @@ describe NotificationService, :mailer do
end
 
it "emails the note author if they've opted into notifications about their activity" do
add_users_with_subscription(note.project, issue)
add_users(project)
add_user_subscriptions(issue)
reset_delivered_emails!
 
note.author.notified_of_own_activity = true
Loading
Loading
@@ -415,13 +418,15 @@ describe NotificationService, :mailer do
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project, assignees: [assignee]) }
let(:mentioned_issue) { create(:issue, assignees: issue.assignees) }
let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@all mentioned') }
let(:author) { create(:user) }
let(:note) { create(:note_on_issue, author: author, noteable: issue, project_id: issue.project_id, note: '@all mentioned') }
 
before do
build_team(note.project)
build_group(note.project)
note.project.add_maintainer(note.author)
add_users_with_subscription(note.project, issue)
build_team(project)
build_group(project)
add_users(project)
add_user_subscriptions(issue)
project.add_maintainer(author)
reset_delivered_emails!
end
 
Loading
Loading
@@ -473,17 +478,18 @@ describe NotificationService, :mailer do
context 'project snippet note' do
let!(:project) { create(:project, :public) }
let(:snippet) { create(:project_snippet, project: project, author: create(:user)) }
let(:note) { create(:note_on_project_snippet, noteable: snippet, project_id: project.id, note: '@all mentioned') }
let(:author) { create(:user) }
let(:note) { create(:note_on_project_snippet, author: author, noteable: snippet, project_id: project.id, note: '@all mentioned') }
 
before do
build_team(project)
build_group(project)
project.add_maintainer(author)
 
# make sure these users can read the project snippet!
project.add_guest(@u_guest_watcher)
project.add_guest(@u_guest_custom)
add_member_for_parent_group(@pg_watcher, project)
note.project.add_maintainer(note.author)
reset_delivered_emails!
end
 
Loading
Loading
@@ -708,10 +714,11 @@ describe NotificationService, :mailer do
let(:issue) { create :issue, project: project, assignees: [assignee], description: 'cc @participant @unsubscribed_mentioned' }
 
before do
build_team(issue.project)
build_group(issue.project)
build_team(project)
build_group(project)
 
add_users_with_subscription(issue.project, issue)
add_users(project)
add_user_subscriptions(issue)
reset_delivered_emails!
update_custom_notification(:new_issue, @u_guest_custom, resource: project)
update_custom_notification(:new_issue, @u_custom_global)
Loading
Loading
@@ -1281,13 +1288,16 @@ describe NotificationService, :mailer do
let(:project) { create(:project, :public, :repository, namespace: group) }
let(:another_project) { create(:project, :public, namespace: group) }
let(:assignee) { create(:user) }
let(:merge_request) { create :merge_request, source_project: project, assignees: [assignee], description: 'cc @participant' }
let(:assignees) { Array.wrap(assignee) }
let(:author) { create(:user) }
let(:merge_request) { create :merge_request, author: author, source_project: project, assignees: assignees, description: 'cc @participant' }
 
before do
project.add_maintainer(merge_request.author)
merge_request.assignees.each { |assignee| project.add_maintainer(assignee) }
build_team(merge_request.target_project)
add_users_with_subscription(merge_request.target_project, merge_request)
project.add_maintainer(author)
assignees.each { |assignee| project.add_maintainer(assignee) }
build_team(project)
add_users(project)
add_user_subscriptions(merge_request)
update_custom_notification(:new_merge_request, @u_guest_custom, resource: project)
update_custom_notification(:new_merge_request, @u_custom_global)
reset_delivered_emails!
Loading
Loading
@@ -2417,7 +2427,7 @@ describe NotificationService, :mailer do
should_not_email(user, recipients: email_recipients)
end
 
def add_users_with_subscription(project, issuable)
def add_users(project)
@subscriber = create :user
@unsubscriber = create :user
@unsubscribed_mentioned = create :user, username: 'unsubscribed_mentioned'
Loading
Loading
@@ -2429,7 +2439,9 @@ describe NotificationService, :mailer do
project.add_maintainer(@unsubscriber)
project.add_maintainer(@watcher_and_subscriber)
project.add_maintainer(@unsubscribed_mentioned)
end
 
def add_user_subscriptions(issuable)
issuable.subscriptions.create(user: @unsubscribed_mentioned, project: project, subscribed: false)
issuable.subscriptions.create(user: @subscriber, project: project, subscribed: true)
issuable.subscriptions.create(user: @subscribed_participant, project: project, subscribed: true)
Loading
Loading
Loading
Loading
@@ -76,6 +76,30 @@ shared_examples 'a mentionable' do
expect(refs).to include(ext_commit)
end
 
context 'when there are cached markdown fields' do
before do
if subject.is_a?(CacheMarkdownField)
subject.refresh_markdown_cache
end
end
it 'sends in cached markdown fields when appropriate' do
if subject.is_a?(CacheMarkdownField)
expect_next_instance_of(Gitlab::ReferenceExtractor) do |ext|
attrs = subject.class.mentionable_attrs.collect(&:first) & subject.cached_markdown_fields.markdown_fields
attrs.each do |field|
expect(ext).to receive(:analyze).with(subject.send(field), hash_including(rendered: anything))
end
end
expect(subject).not_to receive(:refresh_markdown_cache)
expect(subject).to receive(:cached_markdown_fields).at_least(:once).and_call_original
subject.all_references(author)
end
end
end
it 'creates cross-reference notes' do
mentioned_objects = [mentioned_issue, mentioned_mr, mentioned_commit,
ext_issue, ext_mr, ext_commit]
Loading
Loading
@@ -98,6 +122,33 @@ shared_examples 'an editable mentionable' do
[create(:issue, project: project), create(:issue, project: ext_proj)]
end
 
context 'when there are cached markdown fields' do
before do
if subject.is_a?(CacheMarkdownField)
subject.refresh_markdown_cache
end
end
it 'refreshes markdown cache if necessary' do
subject.save!
set_mentionable_text.call('This is a text')
if subject.is_a?(CacheMarkdownField)
expect_next_instance_of(Gitlab::ReferenceExtractor) do |ext|
subject.cached_markdown_fields.markdown_fields.each do |field|
expect(ext).to receive(:analyze).with(subject.send(field), hash_including(rendered: anything))
end
end
expect(subject).to receive(:refresh_markdown_cache)
expect(subject).to receive(:cached_markdown_fields).at_least(:once).and_call_original
subject.all_references(author)
end
end
end
it 'creates new cross-reference notes when the mentionable text is edited' do
subject.save
subject.create_cross_references!
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment