Commit 87616fca authored by Thong Kuah's avatar Thong Kuah Committed by Kamil Trzciński

Use one marginalia comment instead of two

Also add debug tooling for determining where a transaction came from
parent e83374f4
...@@ -13,7 +13,7 @@ require 'marginalia' ...@@ -13,7 +13,7 @@ require 'marginalia'
# matching against the raw SQL, and prepending the comment prevents color # matching against the raw SQL, and prepending the comment prevents color
# coding from working in the development log. # coding from working in the development log.
Marginalia::Comment.prepend_comment = true if Rails.env.production? Marginalia::Comment.prepend_comment = true if Rails.env.production?
Marginalia::Comment.components = [:application, :correlation_id, :jid, :endpoint_id, :db_config_name] Marginalia::Comment.components = [:application, :correlation_id, :jid, :endpoint_id, :db_config_name, :gitlab_schema]
# As mentioned in https://github.com/basecamp/marginalia/pull/93/files, # As mentioned in https://github.com/basecamp/marginalia/pull/93/files,
# adding :line has some overhead because a regexp on the backtrace has # adding :line has some overhead because a regexp on the backtrace has
......
...@@ -5,18 +5,12 @@ module CrossModificationTransactionMixin ...@@ -5,18 +5,12 @@ module CrossModificationTransactionMixin
class_methods do class_methods do
def transaction(**options, &block) def transaction(**options, &block)
# default value of joinable is true super(**options) do
joinable = options[:joinable].nil? || options[:joinable] if connection.current_transaction.respond_to?(:add_gitlab_schema) && gitlab_schema
connection.current_transaction.add_gitlab_schema(gitlab_schema)
# HACK prepend_comment to get spec/lib/gitlab/database/transaction/observer_spec.rb to pass
marginalia_prepended = Marginalia::Comment.prepend_comment
if gitlab_schema && !marginalia_prepended && joinable
Marginalia.with_annotation("gitlab_schema: #{gitlab_schema}") do
super(**options, &block)
end end
else
super(**options, &block) yield
end end
end end
...@@ -33,4 +27,62 @@ module CrossModificationTransactionMixin ...@@ -33,4 +27,62 @@ module CrossModificationTransactionMixin
end end
end end
module TransactionGitlabSchemaMixin
extend ActiveSupport::Concern
def add_gitlab_schema(schema)
@gitlab_schema = schema # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def materialize!
annotate_with_gitlab_schema do
super
end
end
def rollback
annotate_with_gitlab_schema do
super
end
end
def commit
annotate_with_gitlab_schema do
super
end
end
private
attr_reader :gitlab_schema
# Set marginalia comment to track cross-db transactions
# for BEGIN/SAVEPOINT/COMMIT/RELEASE/ROLLBACK
def annotate_with_gitlab_schema
if gitlab_schema
Gitlab::Marginalia::Comment.with_gitlab_schema(gitlab_schema) do
if ENV['CROSS_DB_MOD_DEBUG']
debug_log(:gitlab_schema, gitlab_schema)
Gitlab::BacktraceCleaner.clean_backtrace(caller).each do |line|
debug_log(:backtrace, line)
end
end
yield
end
else
yield
end
end
def debug_log(label, line)
msg = "CrossDatabaseModification #{label}: --> #{line}"
Rails.logger.debug(msg) # rubocop:disable Gitlab/RailsLogger
end
end
ActiveRecord::Base.prepend(CrossModificationTransactionMixin) if Rails.env.test? ActiveRecord::Base.prepend(CrossModificationTransactionMixin) if Rails.env.test?
ActiveRecord::ConnectionAdapters::RealTransaction.prepend(TransactionGitlabSchemaMixin) if Rails.env.test?
ActiveRecord::ConnectionAdapters::SavepointTransaction.prepend(TransactionGitlabSchemaMixin) if Rails.env.test?
...@@ -27,7 +27,7 @@ module Gitlab ...@@ -27,7 +27,7 @@ module Gitlab
context.merge!({ context.merge!({
transaction_depth_by_db: Hash.new { |h, k| h[k] = 0 }, transaction_depth_by_db: Hash.new { |h, k| h[k] = 0 },
transaction_schema: Hash.new { |h, k| h[k] = 0 }, transaction_schema_by_db: Hash.new { |h, k| h[k] = [] },
modified_tables_by_db: Hash.new { |h, k| h[k] = Set.new } modified_tables_by_db: Hash.new { |h, k| h[k] = Set.new }
}) })
end end
...@@ -43,9 +43,9 @@ module Gitlab ...@@ -43,9 +43,9 @@ module Gitlab
end end
def self.txn_schema(sql) def self.txn_schema(sql)
if sql.include?('gitlab_schema: gitlab_main*/') if sql.include?('gitlab_schema:gitlab_main*/')
:gitlab_main :gitlab_main
elsif sql.include?('gitlab_schema: gitlab_ci*/') elsif sql.include?('gitlab_schema:gitlab_ci*/')
:gitlab_ci :gitlab_ci
end end
end end
...@@ -61,13 +61,14 @@ module Gitlab ...@@ -61,13 +61,14 @@ module Gitlab
context[:transaction_depth_by_db][database] += 1 context[:transaction_depth_by_db][database] += 1
gitlab_txn_schema = txn_schema(sql) gitlab_txn_schema = txn_schema(sql)
context[:transaction_schema][gitlab_txn_schema] += 1 if gitlab_txn_schema context[:transaction_schema_by_db][database] << gitlab_txn_schema if gitlab_txn_schema
return return
elsif self.transaction_end?(parsed) elsif self.transaction_end?(parsed)
context[:transaction_depth_by_db][database] -= 1 context[:transaction_depth_by_db][database] -= 1
if context[:transaction_depth_by_db][database] == 0 if context[:transaction_depth_by_db][database] == 0
context[:modified_tables_by_db][database].clear context[:modified_tables_by_db][database].clear
context[:transaction_schema_by_db][database].clear
elsif context[:transaction_depth_by_db][database] < 0 elsif context[:transaction_depth_by_db][database] < 0
context[:transaction_depth_by_db][database] = 0 context[:transaction_depth_by_db][database] = 0
raise CrossDatabaseModificationAcrossUnsupportedTablesError, "Misaligned cross-DB transactions discovered at query #{sql}. This could be a bug in #{self.class} or a valid issue to investigate. Read more at https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions ." raise CrossDatabaseModificationAcrossUnsupportedTablesError, "Misaligned cross-DB transactions discovered at query #{sql}. This could be a bug in #{self.class} or a valid issue to investigate. Read more at https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions ."
...@@ -75,11 +76,7 @@ module Gitlab ...@@ -75,11 +76,7 @@ module Gitlab
gitlab_txn_schema = txn_schema(sql) gitlab_txn_schema = txn_schema(sql)
if gitlab_txn_schema if gitlab_txn_schema
context[:transaction_schema][gitlab_txn_schema] -= 1 context[:transaction_schema_by_db][database].delete(gitlab_txn_schema)
if context[:transaction_schema][gitlab_txn_schema] <= 0
context[:transaction_schema].delete(gitlab_txn_schema)
end
end end
return return
...@@ -108,7 +105,7 @@ module Gitlab ...@@ -108,7 +105,7 @@ module Gitlab
all_tables = context[:modified_tables_by_db].values.map(&:to_a).flatten all_tables = context[:modified_tables_by_db].values.map(&:to_a).flatten
schemas = ::Gitlab::Database::GitlabSchema.table_schemas(all_tables) schemas = ::Gitlab::Database::GitlabSchema.table_schemas(all_tables)
schemas += context[:transaction_schema].keys schemas += context[:transaction_schema_by_db][database].uniq
if schemas.many? if schemas.many?
message = "Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \ message = "Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \
......
...@@ -4,6 +4,14 @@ ...@@ -4,6 +4,14 @@
module Gitlab module Gitlab
module Marginalia module Marginalia
module Comment module Comment
def self.with_gitlab_schema(gitlab_schema)
Thread.current[:marginalia_gitlab_schema] = gitlab_schema
yield
ensure
Thread.current[:marginalia_gitlab_schema] = nil
end
private private
def jid def jid
...@@ -45,6 +53,10 @@ module Gitlab ...@@ -45,6 +53,10 @@ module Gitlab
def db_config_name def db_config_name
::Gitlab::Database.db_config_name(marginalia_adapter) ::Gitlab::Database.db_config_name(marginalia_adapter)
end end
def gitlab_schema
Thread.current[:marginalia_gitlab_schema]
end
end end
end end
end end
...@@ -14,23 +14,25 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio ...@@ -14,23 +14,25 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
Gitlab::Database::QueryAnalyzer.instance.within { example.run } Gitlab::Database::QueryAnalyzer.instance.within { example.run }
end end
shared_examples 'successful examples' do shared_examples 'successful examples' do |model:|
let(:model) { model }
context 'outside transaction' do context 'outside transaction' do
it { expect { run_queries }.not_to raise_error } it { expect { run_queries }.not_to raise_error }
end end
context 'within transaction' do context "within #{model} transaction" do
xit do it do
Project.transaction do model.transaction do
expect { run_queries }.not_to raise_error expect { run_queries }.not_to raise_error
end end
end end
end end
context 'within nested transaction' do context "within nested #{model} transaction" do
xit do it do
Project.transaction(requires_new: true) do model.transaction(requires_new: true) do
Project.transaction(requires_new: true) do model.transaction(requires_new: true) do
expect { run_queries }.not_to raise_error expect { run_queries }.not_to raise_error
end end
end end
...@@ -38,13 +40,26 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio ...@@ -38,13 +40,26 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
end end
end end
shared_examples 'cross-database modification errors' do |model:|
let(:model) { model }
context "within #{model} transaction" do
it 'raises error' do
model.transaction do
expect { run_queries }.to raise_error /Cross-database data modification/
end
end
end
end
context 'when CI and other tables are read in a transaction' do context 'when CI and other tables are read in a transaction' do
def run_queries def run_queries
pipeline.reload pipeline.reload
project.reload project.reload
end end
include_examples 'successful examples' include_examples 'successful examples', model: Project
include_examples 'successful examples', model: Ci::Pipeline
end end
context 'when only CI data is modified' do context 'when only CI data is modified' do
...@@ -53,23 +68,9 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio ...@@ -53,23 +68,9 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
project.reload project.reload
end end
include_examples 'successful examples' include_examples 'successful examples', model: Ci::Pipeline
context 'in a Project transaction' do include_examples 'cross-database modification errors', model: Project
it 'raises error' do
Project.transaction do
expect { run_queries }.to raise_error /Cross-database data modification/
end
end
end
context 'in a CI transaction' do
it 'does not raise error' do
Ci::Pipeline.transaction do
expect { run_queries }.not_to raise_error
end
end
end
end end
context 'when other data is modified' do context 'when other data is modified' do
...@@ -78,7 +79,9 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio ...@@ -78,7 +79,9 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
project.touch project.touch
end end
include_examples 'successful examples' include_examples 'successful examples', model: Project
include_examples 'cross-database modification errors', model: Ci::Pipeline
end end
context 'when both CI and other data is modified' do context 'when both CI and other data is modified' do
...@@ -160,7 +163,9 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio ...@@ -160,7 +163,9 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
project.save! project.save!
end end
include_examples 'successful examples' include_examples 'successful examples', model: Ci::Pipeline
include_examples 'cross-database modification errors', model: Project
end end
describe '.allow_cross_database_modification_within_transaction' do describe '.allow_cross_database_modification_within_transaction' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment