2022-01-20 16:14:18 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
module CrossDatabaseModification
|
|
|
|
extend ActiveSupport::Concern
|
|
|
|
|
|
|
|
class TransactionStackTrackRecord
|
2022-02-10 01:17:41 -05:00
|
|
|
DEBUG_STACK = Rails.env.test? && ENV['DEBUG_GITLAB_TRANSACTION_STACK']
|
|
|
|
LOG_FILENAME = Rails.root.join("log", "gitlab_transaction_stack.log")
|
|
|
|
|
2022-02-11 16:12:17 -05:00
|
|
|
EXCLUDE_DEBUG_TRACE = %w[
|
|
|
|
lib/gitlab/database/query_analyzer
|
|
|
|
app/models/concerns/cross_database_modification.rb
|
|
|
|
].freeze
|
|
|
|
|
2022-02-10 01:17:41 -05:00
|
|
|
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)
|
2022-02-11 16:12:17 -05:00
|
|
|
.reject { |line| EXCLUDE_DEBUG_TRACE.any? { |exclusion| line.include?(exclusion) } }
|
2022-02-10 01:17:41 -05:00
|
|
|
.first(5)
|
|
|
|
|
|
|
|
logger.warn({
|
|
|
|
message: message,
|
|
|
|
action: action,
|
|
|
|
gitlab_transactions_stack: ::ApplicationRecord.gitlab_transactions_stack,
|
|
|
|
caller: cleaned_backtrace,
|
|
|
|
thread: Thread.current.object_id
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
2022-01-20 16:14:18 -05:00
|
|
|
def initialize(subject, gitlab_schema)
|
|
|
|
@subject = subject
|
|
|
|
@gitlab_schema = gitlab_schema
|
|
|
|
@subject.gitlab_transactions_stack.push(gitlab_schema)
|
2022-02-10 01:17:41 -05:00
|
|
|
|
|
|
|
self.class.log_gitlab_transactions_stack(action: :after_push)
|
2022-01-20 16:14:18 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def done!
|
|
|
|
unless @done
|
|
|
|
@done = true
|
2022-02-10 01:17:41 -05:00
|
|
|
|
|
|
|
self.class.log_gitlab_transactions_stack(action: :before_pop)
|
2022-01-20 16:14:18 -05:00
|
|
|
@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)
|
|
|
|
if track_gitlab_schema_in_current_transaction?
|
|
|
|
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
|
|
|
|
else
|
|
|
|
super(**options, &block)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def track_gitlab_schema_in_current_transaction?
|
|
|
|
return false unless Feature::FlipperFeature.table_exists?
|
|
|
|
|
2022-05-06 11:09:03 -04:00
|
|
|
Feature.enabled?(:track_gitlab_schema_in_current_transaction)
|
2022-01-20 16:14:18 -05:00
|
|
|
rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad
|
|
|
|
false
|
|
|
|
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
|