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

json_cache.rb « cache « gitlab « lib - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 7450c7e540bd9d49889d8873d9674cba097ffc73 (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
# frozen_string_literal: true

module Gitlab
  module Cache
    class JsonCache
      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 expire(key)
        backend.delete(cache_key(key))
      end

      def read(key, klass = nil)
        value = read_raw(key)
        value = parse_value(value, klass) unless value.nil?
        value
      end

      def write(key, value, options = nil)
        write_raw(key, value, options)
      end

      def fetch(key, options = {})
        klass = options.delete(:as)
        value = read(key, klass)

        return value unless value.nil?

        value = yield

        write(key, value, options)

        value
      end

      private

      attr_reader :backend, :namespace, :cache_key_strategy

      def cache_key(key)
        expanded_cache_key(key).compact.join(':').freeze
      end

      def write_raw(_key, _value, _options)
        raise NoMethodError
      end

      def expanded_cache_key(_key)
        raise NoMethodError
      end

      def read_raw(_key)
        raise NoMethodError
      end

      def parse_value(value, klass)
        case value
        when Hash then parse_entry(value, klass)
        when Array then parse_entries(value, klass)
        else
          value
        end
      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 because 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
end