Commit 452d554d authored by pbair's avatar pbair

New approach passing connection into shared models

Rework multi-database partitioning code to not use establish_connection,
but instead use the connection from the partitioned model, which should
already be setup for the correct database.
parent 3c56f3aa
# frozen_string_literal: true # frozen_string_literal: true
module Postgresql module Postgresql
class DetachedPartition < ApplicationRecord class DetachedPartition < ::Gitlab::Database::SharedModel
scope :ready_to_drop, -> { where('drop_after < ?', Time.current) } scope :ready_to_drop, -> { where('drop_after < ?', Time.current) }
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
Gitlab::Database::Partitioning.register_models([
AuditEvent,
WebHookLog,
LooseForeignKeys::DeletedRecord
])
if Gitlab.ee?
Gitlab::Database::Partitioning.register_models([
IncidentManagement::PendingEscalations::Alert,
IncidentManagement::PendingEscalations::Issue
])
end
begin begin
Gitlab::Database::Partitioning.sync_partitions unless ENV['DISABLE_POSTGRES_PARTITION_CREATION_ON_STARTUP'] Gitlab::Database::Partitioning.sync_partitions unless ENV['DISABLE_POSTGRES_PARTITION_CREATION_ON_STARTUP']
rescue ActiveRecord::ActiveRecordError, PG::Error rescue ActiveRecord::ActiveRecordError, PG::Error
......
...@@ -3,28 +3,16 @@ ...@@ -3,28 +3,16 @@
module Gitlab module Gitlab
module Database module Database
module Partitioning module Partitioning
def self.sync_partitions(partitioned_models = default_partitioned_models) def self.register_models(models)
MultiDatabasePartitionManager.new(partitioned_models).sync_partitions registered_models.merge(models)
end end
def self.default_partitioned_models def self.registered_models
@default_partitioned_models ||= core_partitioned_models.union(ee_partitioned_models) @registered_models ||= Set.new
end end
def self.core_partitioned_models def self.sync_partitions(models_to_sync = registered_models)
@core_partitioned_models ||= Set[ MultiDatabasePartitionManager.new(models_to_sync).sync_partitions
::AuditEvent,
::WebHookLog
].freeze
end
def self.ee_partitioned_models
return Set.new.freeze unless Gitlab.ee?
@ee_partitioned_models ||= Set[
::IncidentManagement::PendingEscalations::Alert,
::IncidentManagement::PendingEscalations::Issue
].freeze
end end
end end
end end
......
...@@ -9,35 +9,27 @@ module Gitlab ...@@ -9,35 +9,27 @@ module Gitlab
end end
def sync_partitions def sync_partitions
return if models.empty? Gitlab::AppLogger.info(message: "Syncing dynamic postgres partitions")
each_database_connection do models.each do |model|
PartitionManager.new(models).sync_partitions Gitlab::Database::SharedModel.using_connection(model.connection) do
Gitlab::AppLogger.debug(message: "Switched database connection",
connection_name: connection_name,
table_name: model.table_name)
PartitionManager.new(model).sync_partitions
end
end end
Gitlab::AppLogger.info(message: "Finished sync of dynamic postgres partitions")
end end
private private
attr_reader :models attr_reader :models
def each_database_connection(&block) def connection_name
original_db_config = ActiveRecord::Base.connection_db_config # rubocop:disable Database/MultipleDatabases Gitlab::Database::SharedModel.connection.pool.db_config.name
begin
with_each_connection(&block)
ensure
ActiveRecord::Base.establish_connection(original_db_config) # rubocop:disable Database/MultipleDatabases
end
end
def with_each_connection
Gitlab::Database.db_config_names.each do |db_name|
config_for_db_name = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, name: db_name) # rubocop:disable Database/MultipleDatabases
ActiveRecord::Base.establish_connection(config_for_db_name)
yield
end
end end
end end
end end
......
...@@ -10,46 +10,45 @@ module Gitlab ...@@ -10,46 +10,45 @@ module Gitlab
MANAGEMENT_LEASE_KEY = 'database_partition_management_%s' MANAGEMENT_LEASE_KEY = 'database_partition_management_%s'
RETAIN_DETACHED_PARTITIONS_FOR = 1.week RETAIN_DETACHED_PARTITIONS_FOR = 1.week
def initialize(models) def initialize(model)
@models = models @model = model
end end
def sync_partitions def sync_partitions
Gitlab::AppLogger.info("Checking state of dynamic postgres partitions") Gitlab::AppLogger.info(message: "Checking state of dynamic postgres partitions", table_name: model.table_name)
models.each do |model| # Double-checking before getting the lease:
# Double-checking before getting the lease: # The prevailing situation is no missing partitions and no extra partitions
# The prevailing situation is no missing partitions and no extra partitions return if missing_partitions.empty? && extra_partitions.empty?
next if missing_partitions(model).empty? && extra_partitions(model).empty?
only_with_exclusive_lease(model, lease_key: MANAGEMENT_LEASE_KEY) do only_with_exclusive_lease(model, lease_key: MANAGEMENT_LEASE_KEY) do
partitions_to_create = missing_partitions(model) partitions_to_create = missing_partitions
create(partitions_to_create) unless partitions_to_create.empty? create(partitions_to_create) unless partitions_to_create.empty?
if Feature.enabled?(:partition_pruning, default_enabled: :yaml) if Feature.enabled?(:partition_pruning, default_enabled: :yaml)
partitions_to_detach = extra_partitions(model) partitions_to_detach = extra_partitions
detach(partitions_to_detach) unless partitions_to_detach.empty? detach(partitions_to_detach) unless partitions_to_detach.empty?
end
end end
rescue StandardError => e
Gitlab::AppLogger.error(message: "Failed to create / detach partition(s)",
table_name: model.table_name,
exception_class: e.class,
exception_message: e.message)
end end
rescue StandardError => e
Gitlab::AppLogger.error(message: "Failed to create / detach partition(s)",
table_name: model.table_name,
exception_class: e.class,
exception_message: e.message)
end end
private private
attr_reader :models attr_reader :model
delegate :connection, to: :model
def missing_partitions(model) def missing_partitions
return [] unless connection.table_exists?(model.table_name) return [] unless connection.table_exists?(model.table_name)
model.partitioning_strategy.missing_partitions model.partitioning_strategy.missing_partitions
end end
def extra_partitions(model) def extra_partitions
return [] unless connection.table_exists?(model.table_name) return [] unless connection.table_exists?(model.table_name)
model.partitioning_strategy.extra_partitions model.partitioning_strategy.extra_partitions
...@@ -111,13 +110,10 @@ module Gitlab ...@@ -111,13 +110,10 @@ module Gitlab
def with_lock_retries(&block) def with_lock_retries(&block)
Gitlab::Database::WithLockRetries.new( Gitlab::Database::WithLockRetries.new(
klass: self.class, klass: self.class,
logger: Gitlab::AppLogger logger: Gitlab::AppLogger,
connection: connection
).run(&block) ).run(&block)
end end
def connection
ActiveRecord::Base.connection
end
end end
end end
end end
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
class PartitionMonitoring class PartitionMonitoring
attr_reader :models attr_reader :models
def initialize(models = Gitlab::Database::Partitioning.default_partitioned_models) def initialize(models = Gitlab::Database::Partitioning.registered_models)
@models = models @models = models
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module Gitlab module Gitlab
module Database module Database
class PostgresForeignKey < ApplicationRecord class PostgresForeignKey < SharedModel
self.primary_key = :oid self.primary_key = :oid
scope :by_referenced_table_identifier, ->(identifier) do scope :by_referenced_table_identifier, ->(identifier) do
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module Gitlab module Gitlab
module Database module Database
class PostgresPartition < ActiveRecord::Base class PostgresPartition < SharedModel
self.primary_key = :identifier self.primary_key = :identifier
belongs_to :postgres_partitioned_table, foreign_key: 'parent_identifier', primary_key: 'identifier' belongs_to :postgres_partitioned_table, foreign_key: 'parent_identifier', primary_key: 'identifier'
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module Gitlab module Gitlab
module Database module Database
class PostgresPartitionedTable < ActiveRecord::Base class PostgresPartitionedTable < SharedModel
DYNAMIC_PARTITION_STRATEGIES = %w[range list].freeze DYNAMIC_PARTITION_STRATEGIES = %w[range list].freeze
self.primary_key = :identifier self.primary_key = :identifier
......
# frozen_string_literal: true
module Gitlab
module Database
class SharedModel < ActiveRecord::Base
self.abstract_class = true
class << self
def using_connection(connection)
raise 'cannot nest connection overrides for shared models' unless overriding_connection.nil?
self.overriding_connection = connection
yield
ensure
self.overriding_connection = nil
end
def connection
if connection = self.overriding_connection
connection
else
super
end
end
private
def overriding_connection
Thread.current[:overriding_connection]
end
def overriding_connection=(connection)
Thread.current[:overriding_connection] = connection
end
end
end
end
end
...@@ -3,66 +3,34 @@ ...@@ -3,66 +3,34 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Database::Partitioning::MultiDatabasePartitionManager, '#sync_partitions' do RSpec.describe Gitlab::Database::Partitioning::MultiDatabasePartitionManager, '#sync_partitions' do
subject(:sync_partitions) { described_class.new(models).sync_partitions } subject(:sync_partitions) { manager.sync_partitions }
let(:models) { [double, double] } let(:manager) { described_class.new(models) }
let(:models) { [model1, model2] }
let(:db_name1) { 'db1' } let(:model1) { double('model1', connection: connection1, table_name: 'table1') }
let(:db_name2) { 'db2' } let(:model2) { double('model2', connection: connection1, table_name: 'table2') }
let(:config1) { 'config1' } let(:connection1) { double('connection1') }
let(:config2) { 'config2' } let(:connection2) { double('connection2') }
let(:configurations) { double }
let(:manager_class) { Gitlab::Database::Partitioning::PartitionManager } let(:target_manager_class) { Gitlab::Database::Partitioning::PartitionManager }
let(:manager1) { double('manager 1') } let(:target_manager1) { double('partition manager') }
let(:manager2) { double('manager 2') } let(:target_manager2) { double('partition manager') }
let(:original_config) { ActiveRecord::Base.connection_db_config }
before do before do
allow(configurations).to receive(:configs_for).with(env_name: Rails.env, name: db_name1).and_return(config1) allow(manager).to receive(:connection_name).and_return('name')
allow(configurations).to receive(:configs_for).with(env_name: Rails.env, name: db_name2).and_return(config2)
allow(Gitlab::Database).to receive(:db_config_names).and_return([db_name1, db_name2])
allow(ActiveRecord::Base).to receive(:configurations).twice.and_return(configurations)
end end
it 'syncs model partitions for each database connection' do it 'syncs model partitions, setting up the appropriate connection for each', :aggregate_failures do
expect(ActiveRecord::Base).to receive(:establish_connection).with(config1).ordered expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(model1.connection).and_yield.ordered
expect(manager_class).to receive(:new).with(models).and_return(manager1).ordered expect(target_manager_class).to receive(:new).with(model1).and_return(target_manager1).ordered
expect(manager1).to receive(:sync_partitions).ordered expect(target_manager1).to receive(:sync_partitions)
expect(ActiveRecord::Base).to receive(:establish_connection).with(config2).ordered expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(model2.connection).and_yield.ordered
expect(manager_class).to receive(:new).with(models).and_return(manager2).ordered expect(target_manager_class).to receive(:new).with(model2).and_return(target_manager2).ordered
expect(manager2).to receive(:sync_partitions).ordered expect(target_manager2).to receive(:sync_partitions)
expect(ActiveRecord::Base).to receive(:establish_connection).with(original_config).ordered
sync_partitions sync_partitions
end end
context 'if an error is raised' do
it 'restores the original connection' do
expect(ActiveRecord::Base).to receive(:establish_connection).with(config1).ordered
expect(manager_class).to receive(:new).with(models).and_return(manager1).ordered
expect(manager1).to receive(:sync_partitions).ordered.and_raise(RuntimeError)
expect(ActiveRecord::Base).to receive(:establish_connection).with(original_config).ordered
expect { sync_partitions }.to raise_error(RuntimeError)
end
end
context 'if no models are given' do
let(:models) { [] }
it 'does nothing, changing no connections' do
expect(ActiveRecord::Base).not_to receive(:establish_connection)
expect(manager_class).not_to receive(:new)
sync_partitions
end
end
end end
...@@ -13,17 +13,17 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do ...@@ -13,17 +13,17 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end end
context 'creating partitions (mocked)' do context 'creating partitions (mocked)' do
subject(:sync_partitions) { described_class.new(models).sync_partitions } subject(:sync_partitions) { described_class.new(model).sync_partitions }
let(:models) { [model] } let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table, connection: connection) }
let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table) }
let(:partitioning_strategy) { double(missing_partitions: partitions, extra_partitions: []) } let(:partitioning_strategy) { double(missing_partitions: partitions, extra_partitions: []) }
let(:connection) { ActiveRecord::Base.connection }
let(:table) { "some_table" } let(:table) { "some_table" }
before do before do
allow(ActiveRecord::Base.connection).to receive(:table_exists?).and_call_original allow(connection).to receive(:table_exists?).and_call_original
allow(ActiveRecord::Base.connection).to receive(:table_exists?).with(table).and_return(true) allow(connection).to receive(:table_exists?).with(table).and_return(true)
allow(ActiveRecord::Base.connection).to receive(:execute).and_call_original allow(connection).to receive(:execute).and_call_original
stub_exclusive_lease(described_class::MANAGEMENT_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT) stub_exclusive_lease(described_class::MANAGEMENT_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT)
end end
...@@ -36,35 +36,23 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do ...@@ -36,35 +36,23 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end end
it 'creates the partition' do it 'creates the partition' do
expect(ActiveRecord::Base.connection).to receive(:execute).with(partitions.first.to_sql) expect(connection).to receive(:execute).with(partitions.first.to_sql)
expect(ActiveRecord::Base.connection).to receive(:execute).with(partitions.second.to_sql) expect(connection).to receive(:execute).with(partitions.second.to_sql)
sync_partitions sync_partitions
end end
context 'error handling with 2 models' do context 'when an error occurs during partition management' do
let(:models) do it 'does not raise an error' do
[ expect(partitioning_strategy).to receive(:missing_partitions).and_raise('this should never happen (tm)')
double(partitioning_strategy: strategy1, table_name: table),
double(partitioning_strategy: strategy2, table_name: table)
]
end
let(:strategy1) { double('strategy1', missing_partitions: nil, extra_partitions: []) }
let(:strategy2) { double('strategy2', missing_partitions: partitions, extra_partitions: []) }
it 'still creates partitions for the second table' do
expect(strategy1).to receive(:missing_partitions).and_raise('this should never happen (tm)')
expect(ActiveRecord::Base.connection).to receive(:execute).with(partitions.first.to_sql)
expect(ActiveRecord::Base.connection).to receive(:execute).with(partitions.second.to_sql)
sync_partitions expect { sync_partitions }.not_to raise_error
end end
end end
end end
context 'creating partitions' do context 'creating partitions' do
subject(:sync_partitions) { described_class.new([my_model]).sync_partitions } subject(:sync_partitions) { described_class.new(my_model).sync_partitions }
let(:connection) { ActiveRecord::Base.connection } let(:connection) { ActiveRecord::Base.connection }
let(:my_model) do let(:my_model) do
...@@ -93,15 +81,15 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do ...@@ -93,15 +81,15 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
context 'detaching partitions (mocked)' do context 'detaching partitions (mocked)' do
subject(:sync_partitions) { manager.sync_partitions } subject(:sync_partitions) { manager.sync_partitions }
let(:manager) { described_class.new(models) } let(:manager) { described_class.new(model) }
let(:models) { [model] } let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table, connection: connection) }
let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table)}
let(:partitioning_strategy) { double(extra_partitions: extra_partitions, missing_partitions: []) } let(:partitioning_strategy) { double(extra_partitions: extra_partitions, missing_partitions: []) }
let(:connection) { ActiveRecord::Base.connection }
let(:table) { "foo" } let(:table) { "foo" }
before do before do
allow(ActiveRecord::Base.connection).to receive(:table_exists?).and_call_original allow(connection).to receive(:table_exists?).and_call_original
allow(ActiveRecord::Base.connection).to receive(:table_exists?).with(table).and_return(true) allow(connection).to receive(:table_exists?).with(table).and_return(true)
stub_exclusive_lease(described_class::MANAGEMENT_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT) stub_exclusive_lease(described_class::MANAGEMENT_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT)
end end
...@@ -123,24 +111,6 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do ...@@ -123,24 +111,6 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
sync_partitions sync_partitions
end end
context 'error handling' do
let(:models) do
[
double(partitioning_strategy: error_strategy, table_name: table),
model
]
end
let(:error_strategy) { double(extra_partitions: nil, missing_partitions: []) }
it 'still drops partitions for the other model' do
expect(error_strategy).to receive(:extra_partitions).and_raise('injected error!')
extra_partitions.each { |p| expect(manager).to receive(:detach_one_partition).with(p) }
sync_partitions
end
end
end end
context 'with the partition_pruning feature flag disabled' do context 'with the partition_pruning feature flag disabled' do
...@@ -163,7 +133,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do ...@@ -163,7 +133,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end end
end end
subject { described_class.new([my_model]).sync_partitions } subject { described_class.new(my_model).sync_partitions }
let(:connection) { ActiveRecord::Base.connection } let(:connection) { ActiveRecord::Base.connection }
let(:my_model) do let(:my_model) do
...@@ -272,11 +242,11 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do ...@@ -272,11 +242,11 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
it 'creates partitions for the future then drops the oldest one after a month' do it 'creates partitions for the future then drops the oldest one after a month' do
# 1 month for the current month, 1 month for the old month that we're retaining data for, headroom # 1 month for the current month, 1 month for the old month that we're retaining data for, headroom
expected_num_partitions = (Gitlab::Database::Partitioning::MonthlyStrategy::HEADROOM + 2.months) / 1.month expected_num_partitions = (Gitlab::Database::Partitioning::MonthlyStrategy::HEADROOM + 2.months) / 1.month
expect { described_class.new([my_model]).sync_partitions }.to change { num_partitions(my_model) }.from(0).to(expected_num_partitions) expect { described_class.new(my_model).sync_partitions }.to change { num_partitions(my_model) }.from(0).to(expected_num_partitions)
travel 1.month travel 1.month
expect { described_class.new([my_model]).sync_partitions }.to change { has_partition(my_model, 2.months.ago.beginning_of_month) }.from(true).to(false).and(change { num_partitions(my_model) }.by(0)) expect { described_class.new(my_model).sync_partitions }.to change { has_partition(my_model, 2.months.ago.beginning_of_month) }.from(true).to(false).and(change { num_partitions(my_model) }.by(0))
end end
end end
end end
...@@ -8,9 +8,9 @@ RSpec.describe Gitlab::Database::Partitioning do ...@@ -8,9 +8,9 @@ RSpec.describe Gitlab::Database::Partitioning do
let(:partition_manager) { double('partition manager') } let(:partition_manager) { double('partition manager') }
context 'when no partitioned models are given' do context 'when no partitioned models are given' do
it 'calls the partition manager with the default partitions' do it 'calls the partition manager with the registered models' do
expect(partition_manager_class).to receive(:new) expect(partition_manager_class).to receive(:new)
.with(described_class.default_partitioned_models) .with(described_class.registered_models)
.and_return(partition_manager) .and_return(partition_manager)
expect(partition_manager).to receive(:sync_partitions) expect(partition_manager).to receive(:sync_partitions)
...@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Database::Partitioning do ...@@ -20,7 +20,7 @@ RSpec.describe Gitlab::Database::Partitioning do
end end
context 'when partitioned models are given' do context 'when partitioned models are given' do
it 'calls the partition manager with the given partitions' do it 'calls the partition manager with the given models' do
models = ['my special model'] models = ['my special model']
expect(partition_manager_class).to receive(:new) expect(partition_manager_class).to receive(:new)
...@@ -33,15 +33,4 @@ RSpec.describe Gitlab::Database::Partitioning do ...@@ -33,15 +33,4 @@ RSpec.describe Gitlab::Database::Partitioning do
end end
end end
end end
describe '.default_partitioned_models' do
subject(:default_partitioned_models) { described_class.default_partitioned_models }
it 'returns all core and EE models' do
core_models = described_class.core_partitioned_models
ee_models = described_class.ee_partitioned_models
expect(default_partitioned_models).to eq(core_models.union(ee_models))
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::SharedModel do
describe 'using an external connection' do
let!(:original_connection) { described_class.connection }
let(:new_connection) { double('connection') }
it 'overrides the connection for the duration of the block', :aggregate_failures do
expect_original_connection_around do
described_class.using_connection(new_connection) do
expect(described_class.connection).to be(new_connection)
end
end
end
it 'does not affect connections in other threads', :aggregate_failures do
expect_original_connection_around do
described_class.using_connection(new_connection) do
expect(described_class.connection).to be(new_connection)
Thread.new do
expect(described_class.connection).not_to be(new_connection)
end.join
end
end
end
context 'when the block raises an error', :aggregate_failures do
it 're-raises the error, removing the overridden connection' do
expect_original_connection_around do
expect do
described_class.using_connection(new_connection) do
expect(described_class.connection).to be(new_connection)
raise 'here comes an error!'
end
end.to raise_error(RuntimeError, 'here comes an error!')
end
end
end
def expect_original_connection_around
# For safety, ensure our original connection is distinct from our double
# This should be the case, but in case of something leaking we should verify
expect(original_connection).not_to be(new_connection)
expect(described_class.connection).to be(original_connection)
yield
expect(described_class.connection).to be(original_connection)
end
end
end
...@@ -30,7 +30,7 @@ module MigrationsHelpers ...@@ -30,7 +30,7 @@ module MigrationsHelpers
end end
end end
klass.tap { Gitlab::Database::Partitioning::PartitionManager.new([klass]).sync_partitions } klass.tap { Gitlab::Database::Partitioning.sync_partitions([klass]) }
end end
def migrations_paths def migrations_paths
......
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