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

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

module Gitlab
  module Database
    # The purpose of this class is to implement a various query analyzers based on `pg_query`
    # And process them all via `Gitlab::Database::QueryAnalyzers::*`
    #
    # Sometimes this might cause errors in specs.
    # This is best to be disable with `describe '...', query_analyzers: false do`
    class QueryAnalyzer
      include ::Singleton

      Parsed = Struct.new(
        :sql, :connection, :pg
      )

      attr_reader :all_analyzers

      def initialize
        @all_analyzers = []
      end

      def hook!
        @subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |event|
          # In some cases analyzer code might trigger another SQL call
          # to avoid stack too deep this detects recursive call of subscriber
          with_ignored_recursive_calls do
            process_sql(event.payload[:sql], event.payload[:connection])
          end
        end
      end

      def within(user_analyzers = nil)
        # Due to singleton nature of analyzers
        # only an outer invocation of the `.within`
        # is allowed to initialize them
        if already_within?
          raise 'Query analyzers are already defined, cannot re-define them.' if user_analyzers

          return yield
        end

        begin!(user_analyzers || all_analyzers)

        begin
          yield
        ensure
          end!
        end
      end

      def already_within?
        # If analyzers are set they are already configured
        !enabled_analyzers.nil?
      end

      def process_sql(sql, connection)
        analyzers = enabled_analyzers
        return unless analyzers&.any?

        parsed = parse(sql, connection)
        return unless parsed

        analyzers.each do |analyzer|
          next if analyzer.suppressed? && !analyzer.requires_tracking?(parsed)

          analyzer.analyze(parsed)
        rescue StandardError, ::Gitlab::Database::QueryAnalyzers::Base::QueryAnalyzerError => e
          # We catch all standard errors to prevent validation errors to introduce fatal errors in production
          Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
        end
      end

      # Enable query analyzers
      def begin!(analyzers = all_analyzers)
        analyzers = analyzers.select do |analyzer|
          if analyzer.enabled?
            analyzer.begin!

            true
          end
        rescue StandardError, ::Gitlab::Database::QueryAnalyzers::Base::QueryAnalyzerError => e
          Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)

          false
        end

        Thread.current[:query_analyzer_enabled_analyzers] = analyzers
      end

      # Disable enabled query analyzers
      def end!
        enabled_analyzers.select do |analyzer|
          analyzer.end!
        rescue StandardError, ::Gitlab::Database::QueryAnalyzers::Base::QueryAnalyzerError => e
          Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
        end

        Thread.current[:query_analyzer_enabled_analyzers] = nil
      end

      private

      def enabled_analyzers
        Thread.current[:query_analyzer_enabled_analyzers]
      end

      def parse(sql, connection)
        parsed = PgQuery.parse(sql)
        return unless parsed

        normalized = PgQuery.normalize(sql)
        Parsed.new(normalized, connection, parsed)
      rescue PgQuery::ParseError => e
        # Ignore PgQuery parse errors (due to depth limit or other reasons)
        Gitlab::ErrorTracking.track_exception(e)

        nil
      end

      def with_ignored_recursive_calls
        return if Thread.current[:query_analyzer_recursive]

        begin
          Thread.current[:query_analyzer_recursive] = true
          yield
        ensure
          Thread.current[:query_analyzer_recursive] = nil
        end
      end
    end
  end
end