257 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			257 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Ruby
		
	
	
	
# frozen_string_literal: true
 | 
						|
 | 
						|
module Gitlab
 | 
						|
  module RepositoryCacheAdapter
 | 
						|
    extend ActiveSupport::Concern
 | 
						|
    include Gitlab::Utils::StrongMemoize
 | 
						|
 | 
						|
    class_methods do
 | 
						|
      # Caches and strongly memoizes the method.
 | 
						|
      #
 | 
						|
      # This only works for methods that do not take any arguments.
 | 
						|
      #
 | 
						|
      # name     - The name of the method to be cached.
 | 
						|
      # fallback - A value to fall back to if the repository does not exist, or
 | 
						|
      #            in case of a Git error. Defaults to nil.
 | 
						|
      def cache_method(name, fallback: nil)
 | 
						|
        uncached_name = alias_uncached_method(name)
 | 
						|
 | 
						|
        define_method(name) do
 | 
						|
          cache_method_output(name, fallback: fallback) do
 | 
						|
            __send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend
 | 
						|
          end
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      # Caches and strongly memoizes the method as a Redis Set.
 | 
						|
      #
 | 
						|
      # This only works for methods that do not take any arguments. The method
 | 
						|
      # should return an Array of Strings to be cached.
 | 
						|
      #
 | 
						|
      # In addition to overriding the named method, a "name_include?" method is
 | 
						|
      # defined. This uses the "SISMEMBER" query to efficiently check membership
 | 
						|
      # without needing to load the entire set into memory.
 | 
						|
      #
 | 
						|
      # name     - The name of the method to be cached.
 | 
						|
      # fallback - A value to fall back to if the repository does not exist, or
 | 
						|
      #            in case of a Git error. Defaults to nil.
 | 
						|
      #
 | 
						|
      # It is not safe to use this method prior to the release of 12.3, since
 | 
						|
      # 12.2 does not correctly invalidate the redis set cache value. A mixed
 | 
						|
      # code environment containing both 12.2 and 12.3 nodes breaks, while a
 | 
						|
      # mixed code environment containing both 12.3 and 12.4 nodes will work.
 | 
						|
      def cache_method_as_redis_set(name, fallback: nil)
 | 
						|
        uncached_name = alias_uncached_method(name)
 | 
						|
 | 
						|
        define_method(name) do
 | 
						|
          cache_method_output_as_redis_set(name, fallback: fallback) do
 | 
						|
            __send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend
 | 
						|
          end
 | 
						|
        end
 | 
						|
 | 
						|
        # Attempt to determine whether a value is in the set of values being
 | 
						|
        # cached, by performing a redis SISMEMBERS query if appropriate.
 | 
						|
        #
 | 
						|
        # If the full list is already in-memory, we're better using it directly.
 | 
						|
        #
 | 
						|
        # If the cache is not yet populated, querying it directly will give the
 | 
						|
        # wrong answer. We handle that by querying the full list - which fills
 | 
						|
        # the cache - and using it directly to answer the question.
 | 
						|
        define_method("#{name}_include?") do |value|
 | 
						|
          if strong_memoized?(name) || !redis_set_cache.exist?(name)
 | 
						|
            return __send__(name).include?(value) # rubocop:disable GitlabSecurity/PublicSend
 | 
						|
          end
 | 
						|
 | 
						|
          redis_set_cache.include?(name, value)
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      # Caches truthy values from the method. All values are strongly memoized,
 | 
						|
      # and cached in RequestStore.
 | 
						|
      #
 | 
						|
      # Currently only used to cache `exists?` since stale false values are
 | 
						|
      # particularly troublesome. This can occur, for example, when an NFS mount
 | 
						|
      # is temporarily down.
 | 
						|
      #
 | 
						|
      # This only works for methods that do not take any arguments.
 | 
						|
      #
 | 
						|
      # name - The name of the method to be cached.
 | 
						|
      def cache_method_asymmetrically(name)
 | 
						|
        uncached_name = alias_uncached_method(name)
 | 
						|
 | 
						|
        define_method(name) do
 | 
						|
          cache_method_output_asymmetrically(name) do
 | 
						|
            __send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend
 | 
						|
          end
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      # Strongly memoizes the method.
 | 
						|
      #
 | 
						|
      # This only works for methods that do not take any arguments.
 | 
						|
      #
 | 
						|
      # name     - The name of the method to be memoized.
 | 
						|
      # fallback - A value to fall back to if the repository does not exist, or
 | 
						|
      #            in case of a Git error. Defaults to nil. The fallback value
 | 
						|
      #            is not memoized.
 | 
						|
      def memoize_method(name, fallback: nil)
 | 
						|
        uncached_name = alias_uncached_method(name)
 | 
						|
 | 
						|
        define_method(name) do
 | 
						|
          memoize_method_output(name, fallback: fallback) do
 | 
						|
            __send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend
 | 
						|
          end
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      # Prepends "_uncached_" to the target method name
 | 
						|
      #
 | 
						|
      # Returns the uncached method name
 | 
						|
      def alias_uncached_method(name)
 | 
						|
        uncached_name = :"_uncached_#{name}"
 | 
						|
 | 
						|
        alias_method(uncached_name, name)
 | 
						|
 | 
						|
        uncached_name
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    # RequestStore-backed RepositoryCache to be used. Should be overridden by
 | 
						|
    # the including class
 | 
						|
    def request_store_cache
 | 
						|
      raise NotImplementedError
 | 
						|
    end
 | 
						|
 | 
						|
    # RepositoryCache to be used. Should be overridden by the including class
 | 
						|
    def cache
 | 
						|
      raise NotImplementedError
 | 
						|
    end
 | 
						|
 | 
						|
    # RepositorySetCache to be used. Should be overridden by the including class
 | 
						|
    def redis_set_cache
 | 
						|
      raise NotImplementedError
 | 
						|
    end
 | 
						|
 | 
						|
    # RepositoryHashCache to be used. Should be overridden by the including class
 | 
						|
    def redis_hash_cache
 | 
						|
      raise NotImplementedError
 | 
						|
    end
 | 
						|
 | 
						|
    # List of cached methods. Should be overridden by the including class
 | 
						|
    def cached_methods
 | 
						|
      raise NotImplementedError
 | 
						|
    end
 | 
						|
 | 
						|
    # Caches and strongly memoizes the supplied block.
 | 
						|
    #
 | 
						|
    # name     - The name of the method to be cached.
 | 
						|
    # fallback - A value to fall back to if the repository does not exist, or
 | 
						|
    #            in case of a Git error. Defaults to nil.
 | 
						|
    def cache_method_output(name, fallback: nil, &block)
 | 
						|
      memoize_method_output(name, fallback: fallback) do
 | 
						|
        cache.fetch(name, &block)
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    # Caches and strongly memoizes the supplied block as a Redis Set. The result
 | 
						|
    # will be provided as a sorted array.
 | 
						|
    #
 | 
						|
    # name     - The name of the method to be cached.
 | 
						|
    # fallback - A value to fall back to if the repository does not exist, or
 | 
						|
    #            in case of a Git error. Defaults to nil.
 | 
						|
    def cache_method_output_as_redis_set(name, fallback: nil, &block)
 | 
						|
      memoize_method_output(name, fallback: fallback) do
 | 
						|
        redis_set_cache.fetch(name, &block).sort
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    # Caches truthy values from the supplied block. All values are strongly
 | 
						|
    # memoized, and cached in RequestStore.
 | 
						|
    #
 | 
						|
    # Currently only used to cache `exists?` since stale false values are
 | 
						|
    # particularly troublesome. This can occur, for example, when an NFS mount
 | 
						|
    # is temporarily down.
 | 
						|
    #
 | 
						|
    # name - The name of the method to be cached.
 | 
						|
    def cache_method_output_asymmetrically(name, &block)
 | 
						|
      memoize_method_output(name) do
 | 
						|
        request_store_cache.fetch(name) do
 | 
						|
          cache.fetch_without_caching_false(name, &block)
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    # Strongly memoizes the supplied block.
 | 
						|
    #
 | 
						|
    # name     - The name of the method to be memoized.
 | 
						|
    # fallback - A value to fall back to if the repository does not exist, or
 | 
						|
    #            in case of a Git error. Defaults to nil. The fallback value is
 | 
						|
    #            not memoized.
 | 
						|
    def memoize_method_output(name, fallback: nil, &block)
 | 
						|
      no_repository_fallback(name, fallback: fallback) do
 | 
						|
        strong_memoize(memoizable_name(name), &block)
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    # Returns the fallback value if the repository does not exist
 | 
						|
    def no_repository_fallback(name, fallback: nil, &block)
 | 
						|
      # Avoid unnecessary gRPC invocations
 | 
						|
      return fallback if fallback && fallback_early?(name)
 | 
						|
 | 
						|
      yield
 | 
						|
    rescue Gitlab::Git::Repository::NoRepository
 | 
						|
      # Even if the `#exists?` check in `fallback_early?` passes, these errors
 | 
						|
      # might still occur (for example because of a non-existing HEAD). We
 | 
						|
      # want to gracefully handle this and not memoize anything.
 | 
						|
      fallback
 | 
						|
    end
 | 
						|
 | 
						|
    # Expires the caches of a specific set of methods
 | 
						|
    def expire_method_caches(methods)
 | 
						|
      methods.each do |name|
 | 
						|
        unless cached_methods.include?(name.to_sym)
 | 
						|
          Rails.logger.error "Requested to expire non-existent method '#{name}' for Repository" # rubocop:disable Gitlab/RailsLogger
 | 
						|
          next
 | 
						|
        end
 | 
						|
 | 
						|
        cache.expire(name)
 | 
						|
 | 
						|
        clear_memoization(memoizable_name(name))
 | 
						|
      end
 | 
						|
 | 
						|
      expire_redis_set_method_caches(methods)
 | 
						|
      expire_redis_hash_method_caches(methods)
 | 
						|
      expire_request_store_method_caches(methods)
 | 
						|
    end
 | 
						|
 | 
						|
    private
 | 
						|
 | 
						|
    def memoizable_name(name)
 | 
						|
      "#{name.to_s.tr('?!', '')}"
 | 
						|
    end
 | 
						|
 | 
						|
    def expire_request_store_method_caches(methods)
 | 
						|
      methods.each do |name|
 | 
						|
        request_store_cache.expire(name)
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    def expire_redis_set_method_caches(methods)
 | 
						|
      redis_set_cache.expire(*methods)
 | 
						|
    end
 | 
						|
 | 
						|
    def expire_redis_hash_method_caches(methods)
 | 
						|
      methods.each { |name| redis_hash_cache.delete(name) }
 | 
						|
    end
 | 
						|
 | 
						|
    # All cached repository methods depend on the existence of a Git repository,
 | 
						|
    # so if the repository doesn't exist, we already know not to call it.
 | 
						|
    def fallback_early?(method_name)
 | 
						|
      # Avoid infinite loop
 | 
						|
      return false if method_name == :exists?
 | 
						|
 | 
						|
      !exists?
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |