summaryrefslogtreecommitdiff
path: root/lib/gitlab/pages/cache_control.rb
blob: a24d958b7e56f3073fb9faa1503f7b46f846d927 (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
# frozen_string_literal: true

require 'set'

module Gitlab
  module Pages
    class CacheControl
      include Gitlab::Utils::StrongMemoize

      EXPIRE = 12.hours
      # To avoid delivering expired deployment URL in the cached payload,
      # use a longer expiration time in the deployment URL
      DEPLOYMENT_EXPIRATION = (EXPIRE + 12.hours)

      SETTINGS_CACHE_KEY = 'pages_domain_for_%{type}_%{id}'
      PAYLOAD_CACHE_KEY = '%{settings_cache_key}_%{settings_hash}'

      class << self
        def for_domain(domain_id)
          new(type: :domain, id: domain_id)
        end

        def for_namespace(namespace_id)
          new(type: :namespace, id: namespace_id)
        end
      end

      def initialize(type:, id:)
        raise(ArgumentError, "type must be :namespace or :domain") unless %i[namespace domain].include?(type)

        @type = type
        @id = id
      end

      def cache_key
        strong_memoize(:payload_cache_key) do
          cache_settings_hash!

          payload_cache_key_for(settings_hash)
        end
      end

      # Invalidates the cache.
      #
      # Since rails nodes and sidekiq nodes have different application settings,
      # and the invalidation happens in a sidekiq node, we have to use the
      # cached settings hash to build the payload cache key to be invalidated.
      def clear_cache
        keys = cached_settings_hashes
          .map { |hash| payload_cache_key_for(hash) }
          .push(settings_cache_key)

        Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
          Rails.cache.delete_multi(keys)
        end
      end

      private

      # Since rails nodes and sidekiq nodes have different application settings,
      # we cache the application settings hash when creating the payload cache
      # so we can use these values to invalidate the cache in a sidekiq node later.
      def cache_settings_hash!
        cached = cached_settings_hashes.to_set
        Rails.cache.write(settings_cache_key, cached.add(settings_hash))
      end

      def cached_settings_hashes
        Rails.cache.read(settings_cache_key) || []
      end

      def payload_cache_key_for(settings_hash)
        PAYLOAD_CACHE_KEY % {
          settings_cache_key: settings_cache_key,
          settings_hash: settings_hash
        }
      end

      def settings_cache_key
        strong_memoize(:settings_cache_key) do
          SETTINGS_CACHE_KEY % { type: @type, id: @id }
        end
      end

      def settings_hash
        strong_memoize(:settings_hash) do
          values = ::Gitlab.config.pages.dup

          values['app_settings'] = ::Gitlab::CurrentSettings.attributes.slice(
            'force_pages_access_control'
          )

          ::Digest::SHA256.hexdigest(values.inspect)
        end
      end
    end
  end
end