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
|
# frozen_string_literal: true
module CrossDatabaseModification
extend ActiveSupport::Concern
class TransactionStackTrackRecord
DEBUG_STACK = Rails.env.test? && ENV['DEBUG_GITLAB_TRANSACTION_STACK']
LOG_FILENAME = Rails.root.join("log", "gitlab_transaction_stack.log")
EXCLUDE_DEBUG_TRACE = %w[
lib/gitlab/database/query_analyzer
app/models/concerns/cross_database_modification.rb
].freeze
def self.logger
@logger ||= Logger.new(LOG_FILENAME, formatter: ->(_, _, _, msg) { Gitlab::Json.dump(msg) + "\n" })
end
def self.log_gitlab_transactions_stack(action: nil, example: nil)
return unless DEBUG_STACK
message = "gitlab_transactions_stack performing #{action}"
message += " in example #{example}" if example
cleaned_backtrace = Gitlab::BacktraceCleaner.clean_backtrace(caller)
.reject { |line| EXCLUDE_DEBUG_TRACE.any? { |exclusion| line.include?(exclusion) } }
.first(5)
logger.warn({
message: message,
action: action,
gitlab_transactions_stack: ::ApplicationRecord.gitlab_transactions_stack,
caller: cleaned_backtrace,
thread: Thread.current.object_id
})
end
def initialize(subject, gitlab_schema)
@subject = subject
@gitlab_schema = gitlab_schema
@subject.gitlab_transactions_stack.push(gitlab_schema)
self.class.log_gitlab_transactions_stack(action: :after_push)
end
def done!
unless @done
@done = true
self.class.log_gitlab_transactions_stack(action: :before_pop)
@subject.gitlab_transactions_stack.pop
end
true
end
def trigger_transactional_callbacks?
false
end
def before_committed!
end
def rolledback!(force_restore_state: false, should_run_callbacks: true)
done!
end
def committed!(should_run_callbacks: true)
done!
end
end
included do
private_class_method :gitlab_schema
end
class_methods do
def gitlab_transactions_stack
Thread.current[:gitlab_transactions_stack] ||= []
end
def transaction(**options, &block)
super(**options) do
# Hook into current transaction to ensure that once
# the `COMMIT` is executed the `gitlab_transactions_stack`
# will be allowing to execute `after_commit_queue`
record = TransactionStackTrackRecord.new(self, gitlab_schema)
begin
connection.current_transaction.add_record(record)
yield
ensure
record.done!
end
end
end
def gitlab_schema
case self.name
when 'ActiveRecord::Base', 'ApplicationRecord'
:gitlab_main
when 'Ci::ApplicationRecord'
:gitlab_ci
else
Gitlab::Database::GitlabSchema.table_schema(table_name) if table_name
end
end
end
end
|