summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorPatrick Bajao <ebajao@gitlab.com>2019-06-04 20:59:48 -0800
committerPatrick Bajao <ebajao@gitlab.com>2019-06-05 13:19:59 +0800
commit2eecfd8f9d111c6518930b818a16daea8263b37f (patch)
tree12234d26e6e5a950ad69741e761ef7d3d079fdd1 /lib
parentb560ce1e666733f12c65e8b9f659c89256c1775b (diff)
downloadgitlab-ce-2eecfd8f9d111c6518930b818a16daea8263b37f.tar.gz
Use Redis for CacheMarkDownField on non AR models
This allows using `CacheMarkdownField` for models that are not backed by ActiveRecord. When the including class inherits `ActiveRecord::Base` we include `Gitlab::MarkdownCache::ActiveRecord::Extension`. This will cause the markdown fields to be rendered and the generated HTML stored in a `<field>_html` attribute on the record. We also store the version used for generating the markdown. All other classes that include this model will include the `Gitlab::MarkdownCache::Redis::Extension`. This add the `<field>_html` attributes to that model and will generate the html in them. The generated HTML will be cached in redis under the key `markdown_cache:<class>:<id>`. The class this included in must therefore respond to `id`.
Diffstat (limited to 'lib')
-rw-r--r--lib/banzai/commit_renderer.rb2
-rw-r--r--lib/gitlab/markdown_cache.rb12
-rw-r--r--lib/gitlab/markdown_cache/active_record/extension.rb55
-rw-r--r--lib/gitlab/markdown_cache/field_data.rb35
-rw-r--r--lib/gitlab/markdown_cache/redis/extension.rb63
-rw-r--r--lib/gitlab/markdown_cache/redis/store.rb56
6 files changed, 222 insertions, 1 deletions
diff --git a/lib/banzai/commit_renderer.rb b/lib/banzai/commit_renderer.rb
index f346151a3c1..2acc9d13f07 100644
--- a/lib/banzai/commit_renderer.rb
+++ b/lib/banzai/commit_renderer.rb
@@ -2,7 +2,7 @@
module Banzai
module CommitRenderer
- ATTRIBUTES = [:description, :title].freeze
+ ATTRIBUTES = [:description, :title, :full_title].freeze
def self.render(commits, project, user = nil)
obj_renderer = ObjectRenderer.new(user: user, default_project: project)
diff --git a/lib/gitlab/markdown_cache.rb b/lib/gitlab/markdown_cache.rb
new file mode 100644
index 00000000000..0354c710a3f
--- /dev/null
+++ b/lib/gitlab/markdown_cache.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module MarkdownCache
+ # Increment this number every time the renderer changes its output
+ CACHE_COMMONMARK_VERSION_START = 10
+ CACHE_COMMONMARK_VERSION = 16
+
+ BaseError = Class.new(StandardError)
+ UnsupportedClassError = Class.new(BaseError)
+ end
+end
diff --git a/lib/gitlab/markdown_cache/active_record/extension.rb b/lib/gitlab/markdown_cache/active_record/extension.rb
new file mode 100644
index 00000000000..bcc3432bd31
--- /dev/null
+++ b/lib/gitlab/markdown_cache/active_record/extension.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module MarkdownCache
+ module ActiveRecord
+ module Extension
+ extend ActiveSupport::Concern
+
+ included do
+ # Always exclude _html fields from attributes (including serialization).
+ # They contain unredacted HTML, which would be a security issue
+ alias_method :attributes_before_markdown_cache, :attributes
+ def attributes
+ attrs = attributes_before_markdown_cache
+ html_fields = cached_markdown_fields.html_fields
+ whitelisted = cached_markdown_fields.html_fields_whitelisted
+ exclude_fields = html_fields - whitelisted
+
+ exclude_fields.each do |field|
+ attrs.delete(field)
+ end
+
+ if whitelisted.empty?
+ attrs.delete('cached_markdown_version')
+ end
+
+ attrs
+ end
+
+ # Using before_update here conflicts with elasticsearch-model somehow
+ before_create :refresh_markdown_cache, if: :invalidated_markdown_cache?
+ before_update :refresh_markdown_cache, if: :invalidated_markdown_cache?
+ 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
+
+ def markdown_field_changed?(field_name)
+ attribute_changed?(field_name)
+ end
+
+ def save_markdown(updates)
+ return unless persisted? && Gitlab::Database.read_write?
+
+ update_columns(updates)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown_cache/field_data.rb b/lib/gitlab/markdown_cache/field_data.rb
new file mode 100644
index 00000000000..14622c0f186
--- /dev/null
+++ b/lib/gitlab/markdown_cache/field_data.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module MarkdownCache
+ # Knows about the relationship between markdown and html field names, and
+ # stores the rendering contexts for the latter
+ class FieldData
+ def initialize
+ @data = {}
+ end
+
+ delegate :[], :[]=, to: :@data
+
+ def markdown_fields
+ @data.keys
+ end
+
+ def html_field(markdown_field)
+ "#{markdown_field}_html"
+ end
+
+ def html_fields
+ @html_fields ||= markdown_fields.map { |field| html_field(field) }
+ end
+
+ def html_fields_whitelisted
+ markdown_fields.each_with_object([]) do |field, fields|
+ if @data[field].fetch(:whitelisted, false)
+ fields << html_field(field)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown_cache/redis/extension.rb b/lib/gitlab/markdown_cache/redis/extension.rb
new file mode 100644
index 00000000000..97fc23343b4
--- /dev/null
+++ b/lib/gitlab/markdown_cache/redis/extension.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module MarkdownCache
+ module Redis
+ module Extension
+ extend ActiveSupport::Concern
+
+ attr_reader :cached_markdown_version
+
+ class_methods do
+ def cache_markdown_field(markdown_field, context = {})
+ super
+
+ # define the `[field]_html` accessor
+ html_field = cached_markdown_fields.html_field(markdown_field)
+ define_method(html_field) do
+ load_cached_markdown unless markdown_data_loaded?
+
+ instance_variable_get("@#{html_field}")
+ end
+ end
+ end
+
+ private
+
+ def save_markdown(updates)
+ markdown_store.save(updates)
+ end
+
+ def write_markdown_field(field_name, value)
+ instance_variable_set("@#{field_name}", value)
+ end
+
+ def markdown_field_changed?(field_name)
+ false
+ end
+
+ def changed_markdown_fields
+ []
+ end
+
+ def cached_markdown
+ @cached_data ||= markdown_store.read
+ end
+
+ def load_cached_markdown
+ cached_markdown.each do |field_name, value|
+ write_markdown_field(field_name, value)
+ end
+ end
+
+ def markdown_data_loaded?
+ cached_markdown_version.present? || markdown_store.loaded?
+ end
+
+ def markdown_store
+ @store ||= Gitlab::MarkdownCache::Redis::Store.new(self)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown_cache/redis/store.rb b/lib/gitlab/markdown_cache/redis/store.rb
new file mode 100644
index 00000000000..a35eebc6a1b
--- /dev/null
+++ b/lib/gitlab/markdown_cache/redis/store.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module MarkdownCache
+ module Redis
+ class Store
+ EXPIRES_IN = 1.day
+
+ def initialize(subject)
+ @subject = subject
+ @loaded = false
+ end
+
+ def save(updates)
+ @loaded = false
+
+ Gitlab::Redis::Cache.with do |r|
+ r.mapped_hmset(markdown_cache_key, updates)
+ r.expire(markdown_cache_key, EXPIRES_IN)
+ end
+ end
+
+ def read
+ @loaded = true
+
+ results = Gitlab::Redis::Cache.with do |r|
+ r.mapped_hmget(markdown_cache_key, *fields)
+ end
+ # The value read from redis is a string, so we're converting it back
+ # to an int.
+ results[:cached_markdown_version] = results[:cached_markdown_version].to_i
+ results
+ end
+
+ def loaded?
+ @loaded
+ end
+
+ private
+
+ def fields
+ @fields ||= @subject.cached_markdown_fields.html_fields + [:cached_markdown_version]
+ end
+
+ def markdown_cache_key
+ unless @subject.respond_to?(:id)
+ raise Gitlab::MarkdownCache::UnsupportedClassError,
+ "This class has no id to use for caching"
+ end
+
+ "markdown_cache:#{@subject.class}:#{@subject.id}"
+ end
+ end
+ end
+ end
+end