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

Partition manager identifies partitions to detach

Enumerate the partitions that are outside of the retention period, and
for each one log a message that we are planning to detach it.

This way we can run this code in production for a while
and be sure that the choices of partitions to detach are correct.
parent 595f5f64
...@@ -15,7 +15,7 @@ module PartitionedTable ...@@ -15,7 +15,7 @@ module PartitionedTable
@partitioning_strategy = strategy_class.new(self, partitioning_key, **kwargs) @partitioning_strategy = strategy_class.new(self, partitioning_key, **kwargs)
Gitlab::Database::Partitioning::PartitionCreator.register(self) Gitlab::Database::Partitioning::PartitionManager.register(self)
end end
end end
end end
...@@ -10,7 +10,7 @@ class PartitionCreationWorker ...@@ -10,7 +10,7 @@ class PartitionCreationWorker
idempotent! idempotent!
def perform def perform
Gitlab::Database::Partitioning::PartitionCreator.new.create_partitions Gitlab::Database::Partitioning::PartitionManager.new.create_partitions
ensure ensure
Gitlab::Database::Partitioning::PartitionMonitoring.new.report_metrics Gitlab::Database::Partitioning::PartitionMonitoring.new.report_metrics
end end
......
...@@ -3,15 +3,15 @@ ...@@ -3,15 +3,15 @@
# Make sure we have loaded partitioned models here # Make sure we have loaded partitioned models here
# (even with eager loading disabled). # (even with eager loading disabled).
Gitlab::Database::Partitioning::PartitionCreator.register(AuditEvent) Gitlab::Database::Partitioning::PartitionManager.register(AuditEvent)
Gitlab::Database::Partitioning::PartitionCreator.register(WebHookLog) Gitlab::Database::Partitioning::PartitionManager.register(WebHookLog)
if Gitlab.ee? if Gitlab.ee?
Gitlab::Database::Partitioning::PartitionCreator.register(IncidentManagement::PendingEscalations::Alert) Gitlab::Database::Partitioning::PartitionManager.register(IncidentManagement::PendingEscalations::Alert)
end end
begin begin
Gitlab::Database::Partitioning::PartitionCreator.new.create_partitions unless ENV['DISABLE_POSTGRES_PARTITION_CREATION_ON_STARTUP'] Gitlab::Database::Partitioning::PartitionManager.new.create_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
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Gitlab module Gitlab
module Database module Database
module Partitioning module Partitioning
class PartitionCreator class PartitionManager
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)
...@@ -15,7 +15,8 @@ module Gitlab ...@@ -15,7 +15,8 @@ module Gitlab
end end
LEASE_TIMEOUT = 1.minute LEASE_TIMEOUT = 1.minute
LEASE_KEY = 'database_partition_creation_%s' CREATION_LEASE_KEY = 'database_partition_creation_%s'
DETACH_LEASE_KEY = 'database_partition_detach_%s'
attr_reader :models attr_reader :models
...@@ -31,7 +32,7 @@ module Gitlab ...@@ -31,7 +32,7 @@ module Gitlab
# The prevailing situation is no missing partitions # The prevailing situation is no missing partitions
next if missing_partitions(model).empty? next if missing_partitions(model).empty?
only_with_exclusive_lease(model) do only_with_exclusive_creation_lease(model) do
partitions_to_create = missing_partitions(model) partitions_to_create = missing_partitions(model)
next if partitions_to_create.empty? next if partitions_to_create.empty?
...@@ -43,6 +44,22 @@ module Gitlab ...@@ -43,6 +44,22 @@ module Gitlab
end end
end end
def detach_partitions
models.each do |model|
next if extra_partitions(model).empty?
only_with_exclusive_removal_lease(model) do
partitions_to_detach = extra_partitions(model)
next if partitions_to_detach.empty?
detach(partitions_to_detach)
end
rescue StandardError => e
Gitlab::AppLogger.error("Failed to remove partition(s) for #{model.table_name}: #{e.class}: #{e.message}")
end
end
private private
def missing_partitions(model) def missing_partitions(model)
...@@ -51,14 +68,32 @@ module Gitlab ...@@ -51,14 +68,32 @@ module Gitlab
model.partitioning_strategy.missing_partitions model.partitioning_strategy.missing_partitions
end end
def only_with_exclusive_lease(model) def extra_partitions(model)
lease = Gitlab::ExclusiveLease.new(LEASE_KEY % model.table_name, timeout: LEASE_TIMEOUT) return [] unless connection.table_exists?(model.table_name)
model.partitioning_strategy.extra_partitions
end
def only_with_exclusive_lease(model, lease_key:)
lease = Gitlab::ExclusiveLease.new(lease_key % model.table_name, timeout: LEASE_TIMEOUT)
yield if lease.try_obtain yield if lease.try_obtain
ensure ensure
lease&.cancel lease&.cancel
end end
def only_with_exclusive_creation_lease(model)
only_with_exclusive_lease(model, lease_key: CREATION_LEASE_KEY) do
yield
end
end
def only_with_exclusive_removal_lease(model)
only_with_exclusive_lease(model, lease_key: DETACH_LEASE_KEY) do
yield
end
end
def create(model, partitions) def create(model, partitions)
connection.transaction do connection.transaction do
with_lock_retries do with_lock_retries do
...@@ -71,6 +106,18 @@ module Gitlab ...@@ -71,6 +106,18 @@ module Gitlab
end end
end end
def detach(partitions)
connection.transaction do
with_lock_retries do
partitions.each { |p| detach_one_partition(p) }
end
end
end
def detach_one_partition(partition)
Gitlab::AppLogger.info("Planning to detach #{partition.partition_name} for table #{partition.table}")
end
def with_lock_retries(&block) def with_lock_retries(&block)
Gitlab::Database::WithLockRetries.new( Gitlab::Database::WithLockRetries.new(
klass: self.class, klass: self.class,
......
...@@ -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 = PartitionCreator.models) def initialize(models = PartitionManager.models)
@models = models @models = models
end end
......
...@@ -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::PartitionCreator.new.create_partitions Gitlab::Database::Partitioning::PartitionManager.new.create_partitions
end end
# This is targeted towards deploys and upgrades of GitLab. # This is targeted towards deploys and upgrades of GitLab.
......
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Database::Partitioning::PartitionCreator do RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
include Database::PartitioningHelpers include Database::PartitioningHelpers
include Database::TableSchemaHelpers
include ExclusiveLeaseHelpers include ExclusiveLeaseHelpers
describe '.register' do describe '.register' do
...@@ -27,7 +28,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionCreator do ...@@ -27,7 +28,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionCreator 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::CREATION_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT)
end end
let(:partitions) do let(:partitions) do
...@@ -93,4 +94,52 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionCreator do ...@@ -93,4 +94,52 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionCreator do
subject subject
end end
end end
describe '#detach_partitions (mocked)' do
subject { manager.detach_partitions }
let(:manager) { described_class.new(models) }
let(:models) { [model] }
let(:model) { double(partitioning_strategy: partitioning_strategy, table_name: table)}
let(:partitioning_strategy) { double(extra_partitions: extra_partitions) }
let(:table) { "foo" }
before do
allow(ActiveRecord::Base.connection).to receive(:table_exists?).and_call_original
allow(ActiveRecord::Base.connection).to receive(:table_exists?).with(table).and_return(true)
stub_exclusive_lease(described_class::DETACH_LEASE_KEY % table, timeout: described_class::LEASE_TIMEOUT)
end
let(:extra_partitions) do
[
instance_double(Gitlab::Database::Partitioning::TimePartition, table: table, partition_name: 'foo1', to_detach_sql: 'SELECT 1'),
instance_double(Gitlab::Database::Partitioning::TimePartition, table: table, partition_name: 'foo2', to_detach_sql: 'SELECT 2')
]
end
it 'detaches each extra partition' do
extra_partitions.each { |p| expect(manager).to receive(:detach_one_partition).with(p) }
subject
end
context 'error handling' do
let(:models) do
[
double(partitioning_strategy: error_strategy, table_name: table),
model
]
end
let(:error_strategy) { double }
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) }
subject
end
end
end
end end
...@@ -37,7 +37,7 @@ RSpec.describe PartitionedTable do ...@@ -37,7 +37,7 @@ RSpec.describe PartitionedTable do
end end
it 'registers itself with the PartitionCreator' do it 'registers itself with the PartitionCreator' do
expect(Gitlab::Database::Partitioning::PartitionCreator).to receive(:register).with(my_class) expect(Gitlab::Database::Partitioning::PartitionManager).to receive(:register).with(my_class)
subject subject
end end
......
...@@ -6,16 +6,16 @@ RSpec.describe PartitionCreationWorker do ...@@ -6,16 +6,16 @@ RSpec.describe PartitionCreationWorker do
describe '#perform' do describe '#perform' do
subject { described_class.new.perform } subject { described_class.new.perform }
let(:creator) { instance_double('PartitionCreator', create_partitions: nil) } let(:manager) { instance_double('PartitionManager', create_partitions: nil) }
let(:monitoring) { instance_double('PartitionMonitoring', report_metrics: nil) } let(:monitoring) { instance_double('PartitionMonitoring', report_metrics: nil) }
before do before do
allow(Gitlab::Database::Partitioning::PartitionCreator).to receive(:new).and_return(creator) allow(Gitlab::Database::Partitioning::PartitionManager).to receive(:new).and_return(manager)
allow(Gitlab::Database::Partitioning::PartitionMonitoring).to receive(:new).and_return(monitoring) allow(Gitlab::Database::Partitioning::PartitionMonitoring).to receive(:new).and_return(monitoring)
end end
it 'delegates to PartitionCreator' do it 'delegates to PartitionCreator' do
expect(creator).to receive(:create_partitions) expect(manager).to receive(:create_partitions)
subject subject
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