# 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) 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? Feature.enabled?(:track_gitlab_schema_in_current_transaction) 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