# frozen_string_literal: true # module Gitlab module Diff class HighlightCache EXPIRATION = 1.week VERSION = 1 delegate :diffable, to: :@diff_collection delegate :diff_options, to: :@diff_collection def initialize(diff_collection) @diff_collection = diff_collection end # - Reads from cache # - Assigns DiffFile#highlighted_diff_lines for cached files # def decorate(diff_file) if content = read_file(diff_file) diff_file.highlighted_diff_lines = content.map do |line| Gitlab::Diff::Line.init_from_hash(line) end end end # For every file that isn't already contained in the redis hash, store the # result of #highlighted_diff_lines, then submit the uncached content # to #write_to_redis_hash to submit a single write. This avoids excessive # IO generated by N+1's (1 writing for each highlighted line or file). # def write_if_empty return if uncached_files.empty? new_cache_content = {} uncached_files.each do |diff_file| next unless cacheable?(diff_file) new_cache_content[diff_file.file_path] = diff_file.highlighted_diff_lines.map(&:to_hash) end write_to_redis_hash(new_cache_content) end def clear Gitlab::Redis::Cache.with do |redis| redis.del(key) end end def key @redis_key ||= ['highlighted-diff-files', diffable.cache_key, VERSION, diff_options].join(":") end private # We create a Gitlab::Diff::DeprecatedHighlightCache here in order to # expire deprecated cache entries while we make the transition. This can # be removed when :hset_redis_diff_caching is fully launched. # See https://gitlab.com/gitlab-org/gitlab/issues/38008 # def deprecated_cache @deprecated_cache ||= Gitlab::Diff::DeprecatedHighlightCache.new(@diff_collection) end def uncached_files diff_files = @diff_collection.diff_files diff_files.select { |file| read_cache[file.file_path].nil? } end # Given a hash of: # { "file/to/cache" => # [ { line_code: "a5cc2925ca8258af241be7e5b0381edf30266302_19_19", # rich_text: " config/initializers/secret_token.rb\n", # text: " config/initializers/secret_token.rb", # type: nil, # index: 3, # old_pos: 19, # new_pos: 19 } # ] } # # ...it will write/update a Gitlab::Redis hash (HSET) # def write_to_redis_hash(hash) Gitlab::Redis::Cache.with do |redis| redis.pipelined do hash.each do |diff_file_id, highlighted_diff_lines_hash| redis.hset(key, diff_file_id, highlighted_diff_lines_hash.to_json) end # HSETs have to have their expiration date manually updated # redis.expire(key, EXPIRATION) end end # Clean up any deprecated hash entries # deprecated_cache.clear end def file_paths @file_paths ||= @diff_collection.diffs.collect(&:file_path) end def read_file(diff_file) cached_content[diff_file.file_path] end def cached_content @cached_content ||= read_cache end def read_cache return {} unless file_paths.any? results = [] Gitlab::Redis::Cache.with do |redis| results = redis.hmget(key, file_paths) end results.map! do |result| JSON.parse(result, symbolize_names: true) unless result.nil? end file_paths.zip(results).to_h end def cacheable?(diff_file) diffable.present? && diff_file.text? && diff_file.diffable? end end end end