diff --git a/app/workers/database/background_operation/base_scheduler.rb b/app/workers/database/background_operation/base_scheduler.rb new file mode 100644 index 0000000000000000000000000000000000000000..c6394cb3fb2659c53d4e5012b2afe678d511a3cb --- /dev/null +++ b/app/workers/database/background_operation/base_scheduler.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Database # rubocop:disable Gitlab/BoundedContexts -- Database Framework + module BackgroundOperation + module BaseScheduler + extend ActiveSupport::Concern + + include ApplicationWorker + include CronjobQueue # rubocop:disable Scalability/CronWorkerContext -- called from cron + include Database::BackgroundWorkSchedulable + + included do + data_consistency :always + feature_category :database + idempotent! + end + + def self.schedule_feature_flag_name + # TODO; Add a FF in https://gitlab.com/gitlab-org/gitlab/-/issues/577666 + end + + def perform + return unless validate! + + Gitlab::Database::SharedModel.using_connection(base_model.connection) do + break unless self.class.enabled? + + queue_workers_for_execution(queueable_workers) + end + end + + private + + def queueable_workers + raise NotImplementedError, "#{self.class} must define 'queueable_workers'" + end + + def execution_worker_class + raise NotImplementedError, "#{self.class} must define 'execution_worker_class'" + end + + def queue_workers_for_execution(workers) + return unless workers.present? + + jobs_arguments = workers.map { |worker| [tracking_database.to_s, worker.id] } + + execution_worker_class.perform_with_capacity(jobs_arguments) + end + end + end +end diff --git a/app/workers/database/background_work_schedulable.rb b/app/workers/database/background_work_schedulable.rb new file mode 100644 index 0000000000000000000000000000000000000000..8747ca21f22f6c61909c46e188eea6316ec2244b --- /dev/null +++ b/app/workers/database/background_work_schedulable.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Database # rubocop:disable Gitlab/BoundedContexts -- Database Framework + module BackgroundWorkSchedulable + extend ActiveSupport::Concern + + class_methods do + def tracking_database + raise NotImplementedError, "#{name} does not implement #{__method__}" + end + + # rubocop:disable Gitlab/FeatureFlagWithoutActor -- Global FF + # rubocop:disable Gitlab/FeatureFlagKeyDynamic -- It's different for each sub-class. + def enabled? + return false if Feature.enabled?(:disallow_database_ddl_feature_flags, type: :ops) + + schedule_feature_flag_name.present? ? Feature.enabled?(schedule_feature_flag_name, type: :ops) : true + end + # rubocop:enable Gitlab/FeatureFlagWithoutActor + # rubocop:enable Gitlab/FeatureFlagKeyDynamic + end + + included do + include Gitlab::Utils::StrongMemoize + + private + + def validate! + unless base_model + Sidekiq.logger.info( + class: self.class.name, + database: tracking_database, + message: "Skipping #{self.class.name} execution for unconfigured database #{tracking_database}") + + false + end + + return true unless shares_db_config? + + Sidekiq.logger.info( + class: self.class.name, + database: tracking_database, + message: "Skipping #{self.class.name} execution for database that " \ + "shares database configuration with another database") + + false + end + + def max_running_migrations + execution_worker_class.max_running_jobs + end + + def tracking_database + self.class.tracking_database + end + + def base_model + Gitlab::Database.database_base_models[tracking_database] + end + strong_memoize_attr :base_model + + def shares_db_config? + base_model && Gitlab::Database.db_config_share_with(base_model.connection_db_config).present? + end + end + end +end diff --git a/app/workers/database/batched_background_migration/execution_worker.rb b/app/workers/database/batched_background_migration/execution_worker.rb index 33f029a4306576895a64f525961d6b2abb3a80bd..f65fa57475cbbfc45e790ff62829a20fd4fa3758 100644 --- a/app/workers/database/batched_background_migration/execution_worker.rb +++ b/app/workers/database/batched_background_migration/execution_worker.rb @@ -4,6 +4,7 @@ module Database module BatchedBackgroundMigration module ExecutionWorker extend ActiveSupport::Concern + include ExclusiveLeaseGuard include Gitlab::Utils::StrongMemoize include ApplicationWorker diff --git a/app/workers/database/batched_background_migration/single_database_worker.rb b/app/workers/database/batched_background_migration/single_database_worker.rb index f73f8fd751be05a13b372e5321e8a53a5fc2dcf5..892ddc84986ea7afbab1aa84049c9aa36154dbdd 100644 --- a/app/workers/database/batched_background_migration/single_database_worker.rb +++ b/app/workers/database/batched_background_migration/single_database_worker.rb @@ -6,12 +6,14 @@ module SingleDatabaseWorker extend ActiveSupport::Concern include ApplicationWorker - include CronjobQueue # rubocop:disable Scalability/CronWorkerContext - include Gitlab::Utils::StrongMemoize + include CronjobQueue # rubocop:disable Scalability/CronWorkerContext -- called from cron + include Database::BackgroundWorkSchedulable - LEASE_TIMEOUT_MULTIPLIER = 3 - MINIMUM_LEASE_TIMEOUT = 10.minutes.freeze - INTERVAL_VARIANCE = 5.seconds.freeze + class_methods do + def schedule_feature_flag_name + :execute_batched_migrations_on_schedule + end + end included do data_consistency :always @@ -19,42 +21,8 @@ module SingleDatabaseWorker idempotent! end - class_methods do - # :nocov: - def tracking_database - raise NotImplementedError, "#{self.name} does not implement #{__method__}" - end - # :nocov: - - def enabled? - return false if Feature.enabled?(:disallow_database_ddl_feature_flags, type: :ops) - - Feature.enabled?(:execute_batched_migrations_on_schedule, type: :ops) - end - - def lease_key - name.demodulize.underscore - end - end - def perform - unless base_model - Sidekiq.logger.info( - class: self.class.name, - database: tracking_database, - message: 'skipping migration execution for unconfigured database') - - return - end - - if shares_db_config? - Sidekiq.logger.info( - class: self.class.name, - database: tracking_database, - message: 'skipping migration execution for database that shares database configuration with another database') - - return - end + return unless validate! Gitlab::Database::SharedModel.using_connection(base_model.connection) do break unless self.class.enabled? @@ -68,38 +36,11 @@ def perform private - def max_running_migrations - execution_worker_class.max_running_jobs - end - - def tracking_database - self.class.tracking_database - end - def queue_migrations_for_execution(migrations) jobs_arguments = migrations.map { |migration| [tracking_database.to_s, migration.id] } execution_worker_class.perform_with_capacity(jobs_arguments) end - - def base_model - strong_memoize(:base_model) do - Gitlab::Database.database_base_models[tracking_database] - end - end - - def shares_db_config? - base_model && Gitlab::Database.db_config_share_with(base_model.connection_db_config).present? - end - - def with_exclusive_lease(interval) - timeout = [interval * LEASE_TIMEOUT_MULTIPLIER, MINIMUM_LEASE_TIMEOUT].max - lease = Gitlab::ExclusiveLease.new(self.class.lease_key, timeout: timeout) - - yield if lease.try_obtain - ensure - lease&.cancel - end end end end diff --git a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb index 1a492b1dd684df8abf98a8dba81555a38bc62cc9..32fa6cc83f1e5a92d6cb6fa72edc5fb985db64e6 100644 --- a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb +++ b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb @@ -31,18 +31,6 @@ end end - describe '.lease_key' do - let(:lease_key) { described_class.name.demodulize.underscore } - - it 'does not raise an error' do - expect { described_class.lease_key }.not_to raise_error - end - - it 'returns the lease key' do - expect(described_class.lease_key).to eq(lease_key) - end - end - describe '.enabled?' do it 'returns true when execute_batched_migrations_on_schedule feature flag is enabled' do stub_feature_flags(execute_batched_migrations_on_schedule: true) @@ -298,7 +286,9 @@ def perform (number_of_batches + 1).times do described_class.new.perform - travel_to((migration.interval + described_class::INTERVAL_VARIANCE).seconds.from_now) + travel_to( + (migration.interval + Database::BatchedBackgroundMigration::ExecutionWorker::INTERVAL_VARIANCE) + .seconds.from_now) end end