File: cached.rb

package info (click to toggle)
gitlab 17.6.5-19
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 629,368 kB
  • sloc: ruby: 1,915,304; javascript: 557,307; sql: 60,639; xml: 6,509; sh: 4,567; makefile: 1,239; python: 406
file content (91 lines) | stat: -rw-r--r-- 3,030 bytes parent folder | download
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
# frozen_string_literal: true

module Namespaces
  module Traversal
    module Cached
      extend ActiveSupport::Concern
      extend Gitlab::Utils::Override

      included do
        after_destroy :invalidate_descendants_cache
      end

      override :self_and_descendant_ids
      def self_and_descendant_ids(skope: self.class)
        # Cache only works for descendants
        # of the same type as the caller.
        return super unless skope == self.class
        return super unless attempt_to_use_cached_data?

        scope_with_cached_ids(
          super,
          skope,
          Namespaces::Descendants.arel_table[:self_and_descendant_group_ids]
        )
      end

      override :all_project_ids
      def all_project_ids
        return super unless attempt_to_use_cached_data?

        scope_with_cached_ids(
          all_projects.select(:id),
          Project,
          Namespaces::Descendants.arel_table[:all_project_ids]
        )
      end

      private

      # This method implements an OR based cache lookup using COALESCE, similar what you would do in Ruby:
      # return cheap_cached_data || expensive_uncached_data
      def scope_with_cached_ids(consistent_ids_scope, model, cached_ids_column)
        # Look up the cached ids and unnest them into rows if the cache is up to date.
        cache_lookup_query = Namespaces::Descendants
          .where(outdated_at: nil, namespace_id: id)
          .select(cached_ids_column.as('ids'))

        # Invoke the consistent lookup query and collect the ids as a single array value
        consistent_descendant_ids_scope = model
          .from(consistent_ids_scope.arel.as(model.table_name))
          .reselect(Arel::Nodes::NamedFunction.new('ARRAY_AGG', [model.arel_table[:id]]).as('ids'))
          .unscope(where: :type)

        from = <<~SQL
        UNNEST(
          COALESCE(
            (SELECT ids FROM (#{cache_lookup_query.to_sql}) cached_query),
            (SELECT ids FROM (#{consistent_descendant_ids_scope.to_sql}) consistent_query))
        ) AS #{model.table_name}(id)
        SQL

        model
          .from(from)
          .unscope(where: :type)
          .select(:id)
      end

      def attempt_to_use_cached_data?
        Feature.enabled?(:group_hierarchy_optimization, self, type: :beta)
      end

      override :sync_traversal_ids
      def sync_traversal_ids
        super
        return if is_a?(Namespaces::UserNamespace)
        return unless Feature.enabled?(:namespace_descendants_cache_expiration, self, type: :gitlab_com_derisk)

        ids = [id]
        ids.concat((saved_changes[:parent_id] - [parent_id]).compact) if saved_changes[:parent_id]
        Namespaces::Descendants.expire_for(ids)
      end

      def invalidate_descendants_cache
        return if is_a?(Namespaces::UserNamespace)
        return unless Feature.enabled?(:namespace_descendants_cache_expiration, self, type: :gitlab_com_derisk)

        Namespaces::Descendants.expire_for([parent_id, id].compact)
      end
    end
  end
end