discourse/lib/backup_restore/system_interface.rb
Gerhard Schlager d5ef6188ed
PERF: Disable Sidekiq only during database restore (#10857)
It pauses Sidekiq, clears Redis (namespaced to the current site), clears Sidekiq jobs for the current site, restores the database and unpauses Sidekiq. Previously it stayed paused until the end of the restore.

Redis is cleared because we don't want any old data lying around (e.g. old Sidekiq jobs). Most data in Redis is prefixed with the name of the multisite, but Sidekiq jobs in a multisite are all stored in the same keys. So, deleting those jobs requires a little bit more logic.
2020-10-16 15:19:02 +02:00

130 lines
3.5 KiB
Ruby

# frozen_string_literal: true
module BackupRestore
class RunningSidekiqJobsError < RuntimeError
def initialize
super("Sidekiq did not finish running all the jobs in the allowed time!")
end
end
class SystemInterface
delegate :log, to: :@logger, private: true
def initialize(logger)
@logger = logger
@current_db = RailsMultisite::ConnectionManagement.current_db
@readonly_mode_was_enabled = Discourse.readonly_mode?
end
def enable_readonly_mode
return if @readonly_mode_was_enabled
log "Enabling readonly mode..."
Discourse.enable_readonly_mode
end
def disable_readonly_mode
return if @readonly_mode_was_enabled
log "Disabling readonly mode..."
Discourse.disable_readonly_mode
rescue => ex
log "Something went wrong while disabling readonly mode.", ex
end
def mark_restore_as_running
log "Marking restore as running..."
BackupRestore.mark_as_running!
end
def mark_restore_as_not_running
log "Marking restore as finished..."
BackupRestore.mark_as_not_running!
rescue => ex
log "Something went wrong while marking restore as finished.", ex
end
def listen_for_shutdown_signal
BackupRestore.clear_shutdown_signal!
Thread.new do
while BackupRestore.is_operation_running?
exit if BackupRestore.should_shutdown?
sleep 0.1
end
end
end
def pause_sidekiq(reason)
return if Sidekiq.paused?
log "Pausing sidekiq..."
Sidekiq.pause!(reason)
end
def unpause_sidekiq
return unless Sidekiq.paused?
log "Unpausing sidekiq..."
Sidekiq.unpause!
rescue => ex
log "Something went wrong while unpausing Sidekiq.", ex
end
def wait_for_sidekiq
# Wait at least 6 seconds because the data about workers is updated every 5 seconds
# https://github.com/mperham/sidekiq/wiki/API#workers
max_wait_seconds = 60
wait_seconds = 6.0
log "Waiting up to #{max_wait_seconds} seconds for Sidekiq to finish running jobs..."
max_iterations = (max_wait_seconds / wait_seconds).ceil
iterations = 1
loop do
sleep wait_seconds
break if !sidekiq_has_running_jobs?
iterations += 1
raise RunningSidekiqJobsError.new if iterations > max_iterations
log "Waiting for sidekiq to finish running jobs... ##{iterations}"
end
end
def flush_redis
redis = Discourse.redis
redis.scan_each(match: "*") do |key|
redis.del(key) unless key == SidekiqPauser::PAUSED_KEY
end
end
def clear_sidekiq_queues
Sidekiq::Queue.all.each do |queue|
queue.each { |job| delete_job_if_it_belongs_to_current_site(job) }
end
Sidekiq::RetrySet.new.each { |job| delete_job_if_it_belongs_to_current_site(job) }
Sidekiq::ScheduledSet.new.each { |job| delete_job_if_it_belongs_to_current_site(job) }
Sidekiq::DeadSet.new.each { |job| delete_job_if_it_belongs_to_current_site(job) }
end
protected
def sidekiq_has_running_jobs?
Sidekiq::Workers.new.each do |_, _, work|
args = work&.dig("payload", "args")&.first
current_site_id = args["current_site_id"] if args.present?
return true if current_site_id.blank? || current_site_id == @current_db
end
false
end
def delete_job_if_it_belongs_to_current_site(job)
job.delete if job.args.first&.fetch("current_site_id", nil) == @current_db
end
end
end