Commit eec02105 authored by Simon Tomlinson's avatar Simon Tomlinson Committed by Andreas Brandl

Combine partition creation and detaching

By creating and detaching partitions using the same lease, we can
easily skip the partition sync code if another node is currently
executing it. This is safe because we have six months of headroom for
future partitions and the job to sync partitions runs every six hours
parent 6bb48b5f
...@@ -247,8 +247,8 @@ ...@@ -247,8 +247,8 @@
:idempotent: true :idempotent: true
:tags: :tags:
- :exclude_from_kubernetes - :exclude_from_kubernetes
- :name: cronjob:database_partition_detach - :name: cronjob:database_partition_management
:worker_name: Database::PartitionDetachWorker :worker_name: Database::PartitionManagementWorker
:feature_category: :database :feature_category: :database
:has_external_dependencies: :has_external_dependencies:
:urgency: :low :urgency: :low
......
# frozen_string_literal: true # frozen_string_literal: true
module Database module Database
class PartitionDetachWorker class PartitionManagementWorker
include ApplicationWorker include ApplicationWorker
sidekiq_options retry: 3 sidekiq_options retry: 3
...@@ -11,7 +11,7 @@ module Database ...@@ -11,7 +11,7 @@ module Database
idempotent! idempotent!
def perform def perform
Gitlab::Database::Partitioning::PartitionManager.new.detach_partitions Gitlab::Database::Partitioning::PartitionManager.new.sync_partitions
ensure ensure
Gitlab::Database::Partitioning::PartitionMonitoring.new.report_metrics Gitlab::Database::Partitioning::PartitionMonitoring.new.report_metrics
end end
......
...@@ -10,8 +10,6 @@ class PartitionCreationWorker ...@@ -10,8 +10,6 @@ class PartitionCreationWorker
idempotent! idempotent!
def perform def perform
Gitlab::Database::Partitioning::PartitionManager.new.create_partitions # Removed in favor of Database::PartitionManagementWorker
ensure
Gitlab::Database::Partitioning::PartitionMonitoring.new.report_metrics
end end
end end
...@@ -540,12 +540,9 @@ Settings.cron_jobs['authorized_project_update_periodic_recalculate_worker']['job ...@@ -540,12 +540,9 @@ Settings.cron_jobs['authorized_project_update_periodic_recalculate_worker']['job
Settings.cron_jobs['update_container_registry_info_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['update_container_registry_info_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['update_container_registry_info_worker']['cron'] ||= '0 0 * * *' Settings.cron_jobs['update_container_registry_info_worker']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['update_container_registry_info_worker']['job_class'] = 'UpdateContainerRegistryInfoWorker' Settings.cron_jobs['update_container_registry_info_worker']['job_class'] = 'UpdateContainerRegistryInfoWorker'
Settings.cron_jobs['postgres_dynamic_partitions_creator'] ||= Settingslogic.new({}) Settings.cron_jobs['postgres_dynamic_partitions_manager'] ||= Settingslogic.new({})
Settings.cron_jobs['postgres_dynamic_partitions_creator']['cron'] ||= '21 */6 * * *' Settings.cron_jobs['postgres_dynamic_partitions_manager']['cron'] ||= '21 */6 * * *'
Settings.cron_jobs['postgres_dynamic_partitions_creator']['job_class'] ||= 'PartitionCreationWorker' Settings.cron_jobs['postgres_dynamic_partitions_manager']['job_class'] ||= 'Database::PartitionManagementWorker'
Settings.cron_jobs['postgres_dynamic_partitions_detacher'] ||= Settingslogic.new({})
Settings.cron_jobs['postgres_dynamic_partitions_detacher']['cron'] ||= '25 */6 * * *'
Settings.cron_jobs['postgres_dynamic_partitions_detacher']['job_class'] ||= 'Database::PartitionDetachWorker'
Settings.cron_jobs['ci_platform_metrics_update_cron_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['ci_platform_metrics_update_cron_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['ci_platform_metrics_update_cron_worker']['cron'] ||= '47 9 * * *' Settings.cron_jobs['ci_platform_metrics_update_cron_worker']['cron'] ||= '47 9 * * *'
Settings.cron_jobs['ci_platform_metrics_update_cron_worker']['job_class'] = 'CiPlatformMetricsUpdateCronWorker' Settings.cron_jobs['ci_platform_metrics_update_cron_worker']['job_class'] = 'CiPlatformMetricsUpdateCronWorker'
......
...@@ -11,7 +11,7 @@ if Gitlab.ee? ...@@ -11,7 +11,7 @@ if Gitlab.ee?
end end
begin begin
Gitlab::Database::Partitioning::PartitionManager.new.create_partitions unless ENV['DISABLE_POSTGRES_PARTITION_CREATION_ON_STARTUP'] Gitlab::Database::Partitioning::PartitionManager.new.sync_partitions unless ENV['DISABLE_POSTGRES_PARTITION_CREATION_ON_STARTUP']
rescue ActiveRecord::ActiveRecordError, PG::Error rescue ActiveRecord::ActiveRecordError, PG::Error
# ignore - happens when Rake tasks yet have to create a database, e.g. for testing # ignore - happens when Rake tasks yet have to create a database, e.g. for testing
end end
...@@ -4,8 +4,6 @@ module Gitlab ...@@ -4,8 +4,6 @@ module Gitlab
module Database module Database
module Partitioning module Partitioning
class PartitionManager class PartitionManager
include Gitlab::ExclusiveLeaseHelpers
def self.register(model) def self.register(model)
raise ArgumentError, "Only models with a #partitioning_strategy can be registered." unless model.respond_to?(:partitioning_strategy) raise ArgumentError, "Only models with a #partitioning_strategy can be registered." unless model.respond_to?(:partitioning_strategy)
...@@ -17,7 +15,7 @@ module Gitlab ...@@ -17,7 +15,7 @@ module Gitlab
end end
LEASE_TIMEOUT = 1.minute LEASE_TIMEOUT = 1.minute
LEASE_KEY = 'database_partition_management_%s' MANAGEMENT_LEASE_KEY = 'database_partition_management_%s'
attr_reader :models attr_reader :models
...@@ -25,41 +23,25 @@ module Gitlab ...@@ -25,41 +23,25 @@ module Gitlab
@models = models @models = models
end end
def create_partitions def sync_partitions
Gitlab::AppLogger.info("Checking state of dynamic postgres partitions") Gitlab::AppLogger.info("Checking state of dynamic postgres partitions")
models.each do |model| models.each do |model|
# Double-checking before getting the lease: # Double-checking before getting the lease:
# The prevailing situation is no missing partitions # The prevailing situation is no missing partitions and no extra partitions
next if missing_partitions(model).empty? next if missing_partitions(model).empty? && extra_partitions(model).empty?
only_with_lease_lock(model) do only_with_exclusive_lease(model, lease_key: MANAGEMENT_LEASE_KEY) do
partitions_to_create = missing_partitions(model) partitions_to_create = missing_partitions(model)
create(partitions_to_create) unless partitions_to_create.empty?
next if partitions_to_create.empty? if Feature.enabled?(:partition_pruning_dry_run)
partitions_to_detach = extra_partitions(model)
create(model, partitions_to_create) detach(partitions_to_detach) unless partitions_to_detach.empty?
end end
rescue StandardError => e
Gitlab::AppLogger.error("Failed to create partition(s) for #{model.table_name}: #{e.class}: #{e.message}")
end
end
def detach_partitions
return unless Feature.enabled?(:partition_pruning_dry_run)
models.each do |model|
next if extra_partitions(model).empty?
only_with_lease_lock(model) do
partitions_to_detach = extra_partitions(model)
next if partitions_to_detach.empty?
detach(partitions_to_detach)
end end
rescue StandardError => e rescue StandardError => e
Gitlab::AppLogger.error("Failed to remove partition(s) for #{model.table_name}: #{e.class}: #{e.message}") Gitlab::AppLogger.error("Failed to create / detach partition(s) for #{model.table_name}: #{e.class}: #{e.message}")
end end
end end
...@@ -72,18 +54,21 @@ module Gitlab ...@@ -72,18 +54,21 @@ module Gitlab
end end
def extra_partitions(model) def extra_partitions(model)
return [] unless Feature.enabled?(:partition_pruning_dry_run)
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
end end
def only_with_lease_lock(model) def only_with_exclusive_lease(model, lease_key:)
in_lock(LEASE_KEY % model.table_name, ttl: LEASE_TIMEOUT, sleep_sec: 3.seconds) do lease = Gitlab::ExclusiveLease.new(lease_key % model.table_name, timeout: LEASE_TIMEOUT)
yield
end yield if lease.try_obtain
ensure
lease&.cancel
end end
def create(model, partitions) def create(partitions)
connection.transaction do connection.transaction do
with_lock_retries do with_lock_retries do
partitions.each do |partition| partitions.each do |partition|
......
...@@ -118,7 +118,7 @@ namespace :gitlab do ...@@ -118,7 +118,7 @@ namespace :gitlab do
desc 'Create missing dynamic database partitions' desc 'Create missing dynamic database partitions'
task create_dynamic_partitions: :environment do task create_dynamic_partitions: :environment do
Gitlab::Database::Partitioning::PartitionManager.new.create_partitions Gitlab::Database::Partitioning::PartitionManager.new.sync_partitions
end end
# This is targeted towards deploys and upgrades of GitLab. # This is targeted towards deploys and upgrades of GitLab.
......
...@@ -15,12 +15,12 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do ...@@ -15,12 +15,12 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end end
end end
describe '#create_partitions (mocked)' do context 'creating partitions (mocked)' do
subject { described_class.new(models).create_partitions } subject { described_class.new(models).sync_partitions }
let(:models) { [model] } let(:models) { [model] }
let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table) } let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table) }
let(:partitioning_strategy) { double(missing_partitions: partitions) } let(:partitioning_strategy) { double(missing_partitions: partitions, extra_partitions: []) }
let(:table) { "some_table" } let(:table) { "some_table" }
before do before do
...@@ -28,7 +28,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do ...@@ -28,7 +28,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
allow(ActiveRecord::Base.connection).to receive(:table_exists?).with(table).and_return(true) allow(ActiveRecord::Base.connection).to receive(:table_exists?).with(table).and_return(true)
allow(ActiveRecord::Base.connection).to receive(:execute).and_call_original allow(ActiveRecord::Base.connection).to receive(:execute).and_call_original
stub_exclusive_lease(described_class::LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT) stub_exclusive_lease(described_class::MANAGEMENT_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT)
end end
let(:partitions) do let(:partitions) do
...@@ -53,8 +53,8 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do ...@@ -53,8 +53,8 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
] ]
end end
let(:strategy1) { double('strategy1', missing_partitions: nil) } let(:strategy1) { double('strategy1', missing_partitions: nil, extra_partitions: []) }
let(:strategy2) { double('strategy2', missing_partitions: partitions) } let(:strategy2) { double('strategy2', missing_partitions: partitions, extra_partitions: []) }
it 'still creates partitions for the second table' do it 'still creates partitions for the second table' do
expect(strategy1).to receive(:missing_partitions).and_raise('this should never happen (tm)') expect(strategy1).to receive(:missing_partitions).and_raise('this should never happen (tm)')
...@@ -66,8 +66,8 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do ...@@ -66,8 +66,8 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end end
end end
describe '#create_partitions' do context 'creating partitions' do
subject { described_class.new([my_model]).create_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
...@@ -95,20 +95,20 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do ...@@ -95,20 +95,20 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end end
end end
describe '#detach_partitions (mocked)' do context 'detaching partitions (mocked)' do
subject { manager.detach_partitions } subject { manager.sync_partitions }
let(:manager) { described_class.new(models) } let(:manager) { described_class.new(models) }
let(:models) { [model] } let(:models) { [model] }
let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table)} let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table)}
let(:partitioning_strategy) { double(extra_partitions: extra_partitions) } let(:partitioning_strategy) { double(extra_partitions: extra_partitions, missing_partitions: []) }
let(:table) { "foo" } let(:table) { "foo" }
before do before do
allow(ActiveRecord::Base.connection).to receive(:table_exists?).and_call_original allow(ActiveRecord::Base.connection).to receive(:table_exists?).and_call_original
allow(ActiveRecord::Base.connection).to receive(:table_exists?).with(table).and_return(true) allow(ActiveRecord::Base.connection).to receive(:table_exists?).with(table).and_return(true)
stub_exclusive_lease(described_class::LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT) stub_exclusive_lease(described_class::MANAGEMENT_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT)
end end
let(:extra_partitions) do let(:extra_partitions) do
...@@ -136,7 +136,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do ...@@ -136,7 +136,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
] ]
end end
let(:error_strategy) { double } let(:error_strategy) { double(extra_partitions: nil, missing_partitions: []) }
it 'still drops partitions for the other model' do it 'still drops partitions for the other model' do
expect(error_strategy).to receive(:extra_partitions).and_raise('injected error!') expect(error_strategy).to receive(:extra_partitions).and_raise('injected error!')
......
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
require "spec_helper" require "spec_helper"
RSpec.describe Database::PartitionDetachWorker do RSpec.describe Database::PartitionManagementWorker do
describe '#perform' do describe '#perform' do
subject { described_class.new.perform } subject { described_class.new.perform }
let(:manager) { instance_double('PartitionManager', detach_partitions: nil) } let(:manager) { instance_double('PartitionManager', sync_partitions: nil) }
let(:monitoring) { instance_double('PartitionMonitoring', report_metrics: nil) } let(:monitoring) { instance_double('PartitionMonitoring', report_metrics: nil) }
before do before do
...@@ -15,7 +15,7 @@ RSpec.describe Database::PartitionDetachWorker do ...@@ -15,7 +15,7 @@ RSpec.describe Database::PartitionDetachWorker do
end end
it 'delegates to PartitionManager' do it 'delegates to PartitionManager' do
expect(manager).to receive(:detach_partitions) expect(manager).to receive(:sync_partitions)
subject subject
end end
......
# frozen_string_literal: true
require "spec_helper"
RSpec.describe PartitionCreationWorker do
describe '#perform' do
subject { described_class.new.perform }
let(:manager) { instance_double('PartitionManager', create_partitions: nil) }
let(:monitoring) { instance_double('PartitionMonitoring', report_metrics: nil) }
before do
allow(Gitlab::Database::Partitioning::PartitionManager).to receive(:new).and_return(manager)
allow(Gitlab::Database::Partitioning::PartitionMonitoring).to receive(:new).and_return(monitoring)
end
it 'delegates to PartitionCreator' do
expect(manager).to receive(:create_partitions)
subject
end
it 'reports partition metrics' do
expect(monitoring).to receive(:report_metrics)
subject
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