Welcome to mirror list, hosted at ThFree Co, Russian Federation.

helpers.rb « cache « gitlab « lib - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 48b6ca59367fd2dfeadec9a46812641c11d64d16 (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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# frozen_string_literal: true

module Gitlab
  module Cache
    module Helpers
      # @return [ActiveSupport::Duration]
      DEFAULT_EXPIRY = 1.day

      # @return [ActiveSupport::Cache::Store]
      def cache
        Rails.cache
      end

      def render_cached(obj_or_collection, with:, cache_context: -> (_) { current_user&.cache_key }, expires_in: Gitlab::Cache::Helpers::DEFAULT_EXPIRY, **presenter_args)
        json =
          if obj_or_collection.is_a?(Enumerable)
            cached_collection(
              obj_or_collection,
              presenter: with,
              presenter_args: presenter_args,
              context: cache_context,
              expires_in: expires_in
            )
          else
            cached_object(
              obj_or_collection,
              presenter: with,
              presenter_args: presenter_args,
              context: cache_context,
              expires_in: expires_in
            )
          end

        render Gitlab::Json::PrecompiledJson.new(json)
      end

      private

      # Optionally uses a `Proc` to add context to a cache key
      #
      # @param object [Object] must respond to #cache_key
      # @param context [Proc] a proc that will be called with the object as an argument, and which should return a
      #                       string or array of strings to be combined into the cache key
      # @return [String]
      def contextual_cache_key(presenter, object, context)
        return object.cache_key if context.nil?

        [presenter.class.name, object.cache_key, context.call(object)].flatten.join(":")
      end

      # Used for fetching or rendering a single object
      #
      # @param object [Object] the object to render
      # @param presenter [Grape::Entity]
      # @param presenter_args [Hash] keyword arguments to be passed to the entity
      # @param context [Proc]
      # @param expires_in [ActiveSupport::Duration, Integer] an expiry time for the cache entry
      # @return [String]
      def cached_object(object, presenter:, presenter_args:, context:, expires_in:)
        misses = 0

        json = cache.fetch(contextual_cache_key(presenter, object, context), expires_in: expires_in) do
          time_action(render_type: :object) do
            misses += 1

            Gitlab::Json.dump(presenter.represent(object, **presenter_args).as_json)
          end
        end

        increment_cache_metric(render_type: :object, total_count: 1, miss_count: misses)

        json
      end

      # Used for fetching or rendering multiple objects
      #
      # @param objects [Enumerable<Object>] the objects to render
      # @param presenter [Grape::Entity]
      # @param presenter_args [Hash] keyword arguments to be passed to the entity
      # @param context [Proc]
      # @param expires_in [ActiveSupport::Duration, Integer] an expiry time for the cache entry
      # @return [Array<String>]
      def cached_collection(collection, presenter:, presenter_args:, context:, expires_in:)
        misses = 0

        json = fetch_multi(presenter, collection, context: context, expires_in: expires_in) do |obj|
          time_action(render_type: :collection) do
            misses += 1

            Gitlab::Json.dump(presenter.represent(obj, **presenter_args).as_json)
          end
        end

        increment_cache_metric(render_type: :collection, total_count: collection.length, miss_count: misses)

        json.values
      end

      # An adapted version of ActiveSupport::Cache::Store#fetch_multi.
      #
      # The original method only provides the missing key to the block,
      # not the missing object, so we have to create a map of cache keys
      # to the objects to allow us to pass the object to the missing value
      # block.
      #
      # The result is that this is functionally identical to `#fetch`.
      def fetch_multi(presenter, *objs, context:, **kwargs)
        objs.flatten!
        map = multi_key_map(presenter, objs, context: context)

        # TODO: `contextual_cache_key` should be constructed based on the guideline https://docs.gitlab.com/ee/development/redis.html#multi-key-commands.
        Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
          cache.fetch_multi(*map.keys, **kwargs) do |key|
            yield map[key]
          end
        end
      end

      # @param objects [Enumerable<Object>] objects which _must_ respond to `#cache_key`
      # @param context [Proc] a proc that can be called to help generate each cache key
      # @return [Hash]
      def multi_key_map(presenter, objects, context:)
        objects.index_by do |object|
          contextual_cache_key(presenter, object, context)
        end
      end

      def increment_cache_metric(render_type:, total_count:, miss_count:)
        return unless Feature.enabled?(:add_timing_to_certain_cache_actions)
        return unless caller_id

        metric_name = :cached_object_operations_total
        hit_count = total_count - miss_count

        current_transaction&.increment(
          metric_name,
          hit_count,
          { caller_id: caller_id, render_type: render_type, cache_hit: true }
        )

        current_transaction&.increment(
          metric_name,
          miss_count,
          { caller_id: caller_id, render_type: render_type, cache_hit: false }
        )
      end

      def time_action(render_type:, &block)
        if Feature.enabled?(:add_timing_to_certain_cache_actions)
          real_start = Gitlab::Metrics::System.monotonic_time

          presented_object = yield

          real_duration_histogram(render_type).observe({}, Gitlab::Metrics::System.monotonic_time - real_start)

          presented_object
        else
          yield
        end
      end

      def real_duration_histogram(render_type)
        Gitlab::Metrics.histogram(
          :gitlab_presentable_object_cacheless_render_real_duration_seconds,
          'Duration of generating presentable objects to be cached in real time',
          { caller_id: caller_id, render_type: render_type },
          [0.1, 0.5, 1, 2]
        )
      end

      def current_transaction
        @current_transaction ||= ::Gitlab::Metrics::WebTransaction.current
      end

      def caller_id
        @caller_id ||= Gitlab::ApplicationContext.current_context_attribute(:caller_id)
      end
    end
  end
end