summaryrefslogtreecommitdiff
path: root/lib/gitlab/repository_cache_adapter.rb
blob: 7f64a8c9e46268d62da175e39e1876a632150cf8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
module Gitlab
  module RepositoryCacheAdapter
    extend ActiveSupport::Concern

    class_methods do
      # Wraps around the given method and caches its output in Redis and an instance
      # variable.
      #
      # This only works for methods that do not take any arguments.
      def cache_method(name, fallback: nil, memoize_only: false)
        original = :"_uncached_#{name}"

        alias_method(original, name)

        define_method(name) do
          cache_method_output(name, fallback: fallback, memoize_only: memoize_only) do
            __send__(original) # rubocop:disable GitlabSecurity/PublicSend
          end
        end
      end
    end

    # RepositoryCache to be used. Should be overridden by the including class
    def cache
      raise NotImplementedError
    end

    # Caches the supplied block both in a cache and in an instance variable.
    #
    # The cache key and instance variable are named the same way as the value of
    # the `key` argument.
    #
    # This method will return `nil` if the corresponding instance variable is also
    # set to `nil`. This ensures we don't keep yielding the block when it returns
    # `nil`.
    #
    # key - The name of the key to cache the data in.
    # fallback - A value to fall back to in the event of a Git error.
    def cache_method_output(key, fallback: nil, memoize_only: false, &block)
      ivar = cache_instance_variable_name(key)

      if instance_variable_defined?(ivar)
        instance_variable_get(ivar)
      else
        # If the repository doesn't exist and a fallback was specified we return
        # that value inmediately. This saves us Rugged/gRPC invocations.
        return fallback unless fallback.nil? || cache.repository.exists?

        begin
          value =
            if memoize_only
              yield
            else
              cache.fetch(key, &block)
            end

          instance_variable_set(ivar, value)
        rescue Gitlab::Git::Repository::NoRepository
          # Even if the above `#exists?` check passes these errors might still
          # occur (for example because of a non-existing HEAD). We want to
          # gracefully handle this and not cache anything
          fallback
        end
      end
    end

    # Expires the caches of a specific set of methods
    def expire_method_caches(methods)
      methods.each do |key|
        cache.expire(key)

        ivar = cache_instance_variable_name(key)

        remove_instance_variable(ivar) if instance_variable_defined?(ivar)
      end
    end

    private

    def cache_instance_variable_name(key)
      :"@#{key.to_s.tr('?!', '')}"
    end
  end
end