Commit 0d00f891 authored by Kamil Trzciński's avatar Kamil Trzciński

Add `QueryAnalyzer::Base.suppressed?` method

This allows to temporarily suppress the analyzer
for any purpose in a consistent way.
parent 15bbaded
...@@ -58,6 +58,8 @@ module Gitlab ...@@ -58,6 +58,8 @@ module Gitlab
return unless parsed return unless parsed
analyzers.each do |analyzer| analyzers.each do |analyzer|
next if analyzer.suppressed?
analyzer.analyze(parsed) analyzer.analyze(parsed)
rescue StandardError => e rescue StandardError => e
# We catch all standard errors to prevent validation errors to introduce fatal errors in production # We catch all standard errors to prevent validation errors to introduce fatal errors in production
......
...@@ -4,16 +4,32 @@ module Gitlab ...@@ -4,16 +4,32 @@ module Gitlab
module Database module Database
module QueryAnalyzers module QueryAnalyzers
class Base class Base
def self.suppressed?
Thread.current[self.suppress_key]
end
def self.suppress=(value)
Thread.current[self.suppress_key] = value
end
def self.with_suppressed(value = true, &blk)
previous = self.suppressed?
self.suppress = value
yield
ensure
self.suppress = previous
end
def self.begin! def self.begin!
Thread.current[self.class.name] = {} Thread.current[self.context_key] = {}
end end
def self.end! def self.end!
Thread.current[self.class.name] = nil Thread.current[self.context_key] = nil
end end
def self.context def self.context
Thread.current[self.class.name] Thread.current[self.context_key]
end end
def self.enabled? def self.enabled?
...@@ -23,6 +39,14 @@ module Gitlab ...@@ -23,6 +39,14 @@ module Gitlab
def self.analyze(parsed) def self.analyze(parsed)
raise NotImplementedError raise NotImplementedError
end end
def self.context_key
"#{self.class.name}_context"
end
def self.suppress_key
"#{self.class.name}_suppressed"
end
end end
end end
end end
......
...@@ -6,23 +6,6 @@ module Gitlab ...@@ -6,23 +6,6 @@ module Gitlab
class PreventCrossDatabaseModification < Database::QueryAnalyzers::Base class PreventCrossDatabaseModification < Database::QueryAnalyzers::Base
CrossDatabaseModificationAcrossUnsupportedTablesError = Class.new(StandardError) CrossDatabaseModificationAcrossUnsupportedTablesError = Class.new(StandardError)
def self.allow_cross_database_modification?
Thread.current[:prevent_cross_database_modification_allowed]
end
def self.allow_cross_database_modification=(value)
Thread.current[:prevent_cross_database_modification_allowed] = value
end
def self.with_allow_cross_database_modification(value, &blk)
previous = self.allow_cross_database_modification?
self.allow_cross_database_modification = value
yield
ensure
self.allow_cross_database_modification = previous
end
# This method will allow cross database modifications within the block # This method will allow cross database modifications within the block
# Example: # Example:
# #
...@@ -30,13 +13,13 @@ module Gitlab ...@@ -30,13 +13,13 @@ module Gitlab
# create(:build) # inserts ci_build and project record in one transaction # create(:build) # inserts ci_build and project record in one transaction
# end # end
def self.allow_cross_database_modification_within_transaction(url:, &blk) def self.allow_cross_database_modification_within_transaction(url:, &blk)
self.with_allow_cross_database_modification(true, &blk) self.with_suppressed(true, &blk)
end end
# This method will prevent cross database modifications within the block # This method will prevent cross database modifications within the block
# if it was allowed previously # if it was allowed previously
def self.with_cross_database_modification_prevented(&blk) def self.with_cross_database_modification_prevented(&blk)
self.with_allow_cross_database_modification(false, &blk) self.with_suppressed(false, &blk)
end end
def self.begin! def self.begin!
...@@ -55,7 +38,6 @@ module Gitlab ...@@ -55,7 +38,6 @@ module Gitlab
# rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/AbcSize
def self.analyze(parsed) def self.analyze(parsed)
return if self.allow_cross_database_modification?
return if in_factory_bot_create? return if in_factory_bot_create?
database = ::Gitlab::Database.db_config_name(parsed.connection) database = ::Gitlab::Database.db_config_name(parsed.connection)
...@@ -112,10 +94,6 @@ module Gitlab ...@@ -112,10 +94,6 @@ module Gitlab
rescue CrossDatabaseModificationAcrossUnsupportedTablesError => e rescue CrossDatabaseModificationAcrossUnsupportedTablesError => e
::Gitlab::ErrorTracking.track_exception(e, { gitlab_schemas: schemas, tables: all_tables, query: parsed.sql }) ::Gitlab::ErrorTracking.track_exception(e, { gitlab_schemas: schemas, tables: all_tables, query: parsed.sql })
raise if raise_exception? raise if raise_exception?
rescue StandardError => e
# Extra safety net to ensure we never raise in production
# if something goes wrong in this logic
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
end end
# rubocop:enable Metrics/AbcSize # rubocop:enable Metrics/AbcSize
......
...@@ -9,6 +9,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzer, query_analyzers: false do ...@@ -9,6 +9,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzer, query_analyzers: false do
before do before do
allow(described_class.instance).to receive(:all_analyzers).and_return([analyzer, disabled_analyzer]) allow(described_class.instance).to receive(:all_analyzers).and_return([analyzer, disabled_analyzer])
allow(analyzer).to receive(:enabled?).and_return(true) allow(analyzer).to receive(:enabled?).and_return(true)
allow(analyzer).to receive(:suppressed?).and_return(false)
allow(analyzer).to receive(:begin!) allow(analyzer).to receive(:begin!)
allow(analyzer).to receive(:end!) allow(analyzer).to receive(:end!)
allow(disabled_analyzer).to receive(:enabled?).and_return(false) allow(disabled_analyzer).to receive(:enabled?).and_return(false)
...@@ -125,6 +126,13 @@ RSpec.describe Gitlab::Database::QueryAnalyzer, query_analyzers: false do ...@@ -125,6 +126,13 @@ RSpec.describe Gitlab::Database::QueryAnalyzer, query_analyzers: false do
expect { process_sql("SELECT 1 FROM projects") }.not_to raise_error expect { process_sql("SELECT 1 FROM projects") }.not_to raise_error
end end
it 'does not call analyze on suppressed analyzers' do
expect(analyzer).to receive(:suppressed?).and_return(true)
expect(analyzer).not_to receive(:analyze)
expect { process_sql("SELECT 1 FROM projects") }.not_to raise_error
end
def process_sql(sql) def process_sql(sql)
described_class.instance.within do described_class.instance.within do
ApplicationRecord.load_balancer.read_write do |connection| ApplicationRecord.load_balancer.read_write do |connection|
......
...@@ -11,14 +11,21 @@ CROSS_DB_MODIFICATION_ALLOW_LIST = Set.new(YAML.load_file(File.join(__dir__, 'cr ...@@ -11,14 +11,21 @@ CROSS_DB_MODIFICATION_ALLOW_LIST = Set.new(YAML.load_file(File.join(__dir__, 'cr
RSpec.configure do |config| RSpec.configure do |config|
config.include(PreventCrossDatabaseModificationSpecHelpers) config.include(PreventCrossDatabaseModificationSpecHelpers)
# By default allow cross-modifications as we want to observe only transactions
# within a specific block of execution which is defined be `before(:each)` and `after(:each)`
config.before(:all) do
::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.suppress = true
end
# Using before and after blocks because the around block causes problems with the let_it_be # Using before and after blocks because the around block causes problems with the let_it_be
# record creations. It makes an extra savepoint which breaks the transaction count logic. # record creations. It makes an extra savepoint which breaks the transaction count logic.
config.before do |example_file| config.before do |example_file|
::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.allow_cross_database_modification = ::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.suppress =
CROSS_DB_MODIFICATION_ALLOW_LIST.include?(example_file.file_path_rerun_argument) CROSS_DB_MODIFICATION_ALLOW_LIST.include?(example_file.file_path_rerun_argument)
end end
# Reset after execution to preferred state
config.after do |example_file| config.after do |example_file|
::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.allow_cross_database_modification = false ::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.suppress = true
end end
end end
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