118 lines
3.1 KiB
Ruby
118 lines
3.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Gitlab
|
|
class JsonCache
|
|
attr_reader :backend, :namespace
|
|
|
|
STRATEGY_KEY_COMPONENTS = {
|
|
revision: Gitlab.revision,
|
|
version: [Gitlab::VERSION, Rails.version]
|
|
}.freeze
|
|
|
|
def initialize(options = {})
|
|
@backend = options.fetch(:backend, Rails.cache)
|
|
@namespace = options.fetch(:namespace, nil)
|
|
@cache_key_strategy = options.fetch(:cache_key_strategy, :revision)
|
|
end
|
|
|
|
def active?
|
|
if backend.respond_to?(:active?)
|
|
backend.active?
|
|
else
|
|
true
|
|
end
|
|
end
|
|
|
|
def cache_key(key)
|
|
expanded_cache_key = [namespace, key, *strategy_key_component].compact
|
|
expanded_cache_key.join(':').freeze
|
|
end
|
|
|
|
def strategy_key_component
|
|
STRATEGY_KEY_COMPONENTS.fetch(@cache_key_strategy)
|
|
end
|
|
|
|
def expire(key)
|
|
backend.delete(cache_key(key))
|
|
end
|
|
|
|
def read(key, klass = nil)
|
|
value = backend.read(cache_key(key))
|
|
value = parse_value(value, klass) unless value.nil?
|
|
value
|
|
end
|
|
|
|
def write(key, value, options = nil)
|
|
backend.write(cache_key(key), value.to_json, options)
|
|
end
|
|
|
|
def fetch(key, options = {}, &block)
|
|
klass = options.delete(:as)
|
|
value = read(key, klass)
|
|
|
|
return value unless value.nil?
|
|
|
|
value = yield
|
|
|
|
write(key, value, options)
|
|
|
|
value
|
|
end
|
|
|
|
private
|
|
|
|
def parse_value(raw, klass)
|
|
value = Gitlab::Json.parse(raw.to_s)
|
|
|
|
case value
|
|
when Hash then parse_entry(value, klass)
|
|
when Array then parse_entries(value, klass)
|
|
else
|
|
value
|
|
end
|
|
rescue JSON::ParserError
|
|
nil
|
|
end
|
|
|
|
def parse_entry(raw, klass)
|
|
return unless valid_entry?(raw, klass)
|
|
return klass.new(raw) unless klass.ancestors.include?(ActiveRecord::Base)
|
|
|
|
# When the cached value is a persisted instance of ActiveRecord::Base in
|
|
# some cases a relation can return an empty collection becauses scope.none!
|
|
# is being applied on ActiveRecord::Associations::CollectionAssociation#scope
|
|
# when the new_record? method incorrectly returns false.
|
|
#
|
|
# See https://gitlab.com/gitlab-org/gitlab/issues/9903#note_145329964
|
|
klass.allocate.init_with(encode_for(klass, raw))
|
|
end
|
|
|
|
def encode_for(klass, raw)
|
|
# We have models that leave out some fields from the JSON export for
|
|
# security reasons, e.g. models that include the CacheMarkdownField.
|
|
# The ActiveRecord::AttributeSet we build from raw does know about
|
|
# these columns so we need manually set them.
|
|
missing_attributes = (klass.columns.map(&:name) - raw.keys)
|
|
missing_attributes.each { |column| raw[column] = nil }
|
|
|
|
coder = {}
|
|
klass.new(raw).encode_with(coder)
|
|
coder["new_record"] = new_record?(raw, klass)
|
|
coder
|
|
end
|
|
|
|
def new_record?(raw, klass)
|
|
raw.fetch(klass.primary_key, nil).blank?
|
|
end
|
|
|
|
def valid_entry?(raw, klass)
|
|
return false unless klass && raw.is_a?(Hash)
|
|
|
|
(raw.keys - klass.attribute_names).empty?
|
|
end
|
|
|
|
def parse_entries(values, klass)
|
|
values.filter_map { |value| parse_entry(value, klass) }
|
|
end
|
|
end
|
|
end
|