summaryrefslogtreecommitdiff
path: root/lib/gitlab/database/transaction/context.rb
blob: a902537f02e81d82402500baa614dd620666ac19 (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
# frozen_string_literal: true

module Gitlab
  module Database
    module Transaction
      class Context
        attr_reader :context

        LOG_SAVEPOINTS_THRESHOLD = 1    # 1 `SAVEPOINT` created in a transaction
        LOG_DURATION_S_THRESHOLD = 120  # transaction that is running for 2 minutes or longer
        LOG_THROTTLE_DURATION = 1

        def initialize
          @context = {}
        end

        def set_start_time
          @context[:start_time] = current_timestamp
        end

        def set_depth(depth)
          @context[:depth] = [@context[:depth].to_i, depth].max
        end

        def increment_savepoints
          @context[:savepoints] = @context[:savepoints].to_i + 1
        end

        def increment_rollbacks
          @context[:rollbacks] = @context[:rollbacks].to_i + 1
        end

        def increment_releases
          @context[:releases] = @context[:releases].to_i + 1
        end

        def track_sql(sql)
          (@context[:queries] ||= []).push(sql)
        end

        def track_backtrace(backtrace)
          cleaned_backtrace = Gitlab::BacktraceCleaner.clean_backtrace(backtrace)
          (@context[:backtraces] ||= []).push(cleaned_backtrace)
        end

        def duration
          return unless @context[:start_time].present?

          current_timestamp - @context[:start_time]
        end

        def savepoints_threshold_exceeded?
          @context[:savepoints].to_i >= LOG_SAVEPOINTS_THRESHOLD
        end

        def duration_threshold_exceeded?
          duration.to_i >= LOG_DURATION_S_THRESHOLD
        end

        def should_log?
          return false if logged_already?

          savepoints_threshold_exceeded? || duration_threshold_exceeded?
        end

        def commit
          log(:commit)
        end

        def rollback
          log(:rollback)
        end

        def backtraces
          @context[:backtraces].to_a
        end

        private

        def queries
          @context[:queries].to_a.join("\n")
        end

        def current_timestamp
          ::Gitlab::Metrics::System.monotonic_time
        end

        def logged_already?
          return false if @context[:last_log_timestamp].nil?

          (current_timestamp - @context[:last_log_timestamp].to_i) < LOG_THROTTLE_DURATION
        end

        def set_last_log_timestamp
          @context[:last_log_timestamp] = current_timestamp
        end

        def log(operation)
          return unless should_log?

          set_last_log_timestamp

          attributes = {
            class: self.class.name,
            result: operation,
            duration_s: duration,
            depth: @context[:depth].to_i,
            savepoints_count: @context[:savepoints].to_i,
            rollbacks_count: @context[:rollbacks].to_i,
            releases_count: @context[:releases].to_i,
            sql: queries,
            savepoint_backtraces: backtraces
          }

          application_info(attributes)
        end

        def application_info(attributes)
          Gitlab::AppJsonLogger.info(attributes)
        end
      end
    end
  end
end