discourse/lib/backup_restore.rb
Osama Sayegh 45ccadeeeb
DEV: Upgrade Rails to 6.1.3.1 (#12688)
Rails 6.1.3.1 deprecates a few API and has some internal changes that break our tests suite, so this commit fixes all the deprecations and errors and now Discourse should be fully compatible with Rails 6.1.3.1. We also have a new release of the rails_failover gem that's compatible with Rails 6.1.3.1.
2021-04-21 12:36:32 +03:00

186 lines
5.1 KiB
Ruby

# frozen_string_literal: true
module BackupRestore
class OperationRunningError < RuntimeError; end
VERSION_PREFIX = "v"
DUMP_FILE = "dump.sql.gz"
LOGS_CHANNEL = "/admin/backups/logs"
def self.backup!(user_id, opts = {})
if opts[:fork] == false
BackupRestore::Backuper.new(user_id, opts).run
else
spawn_process!(:backup, user_id, opts)
end
end
def self.restore!(user_id, opts = {})
spawn_process!(:restore, user_id, opts)
end
def self.rollback!
raise BackupRestore::OperationRunningError if BackupRestore.is_operation_running?
if can_rollback?
move_tables_between_schemas("backup", "public")
end
end
def self.cancel!
set_shutdown_signal!
true
end
def self.mark_as_running!
Discourse.redis.setex(running_key, 60, "1")
save_start_logs_message_id
keep_it_running
end
def self.is_operation_running?
!!Discourse.redis.get(running_key)
end
def self.mark_as_not_running!
Discourse.redis.del(running_key)
end
def self.should_shutdown?
!!Discourse.redis.get(shutdown_signal_key)
end
def self.can_rollback?
backup_tables_count > 0
end
def self.operations_status
{
is_operation_running: is_operation_running?,
can_rollback: can_rollback?,
allow_restore: Rails.env.development? || SiteSetting.allow_restore
}
end
def self.logs
id = start_logs_message_id
MessageBus.backlog(LOGS_CHANNEL, id).map { |m| m.data }
end
def self.current_version
ActiveRecord::Migrator.current_version
end
def self.postgresql_major_version
DB.query_single("SHOW server_version").first[/\d+/].to_i
end
def self.move_tables_between_schemas(source, destination)
owner = database_configuration.username
ActiveRecord::Base.transaction do
DB.exec(move_tables_between_schemas_sql(source, destination, owner))
end
end
def self.move_tables_between_schemas_sql(source, destination, owner)
<<~SQL
DO $$DECLARE row record;
BEGIN
-- create <destination> schema if it does not exists already
-- NOTE: DROP & CREATE SCHEMA is easier, but we don't want to drop the public schema
-- otherwise extensions (like hstore & pg_trgm) won't work anymore...
CREATE SCHEMA IF NOT EXISTS #{destination};
-- move all <source> tables to <destination> schema
FOR row IN SELECT tablename FROM pg_tables WHERE schemaname = '#{source}' AND tableowner = '#{owner}'
LOOP
EXECUTE 'DROP TABLE IF EXISTS #{destination}.' || quote_ident(row.tablename) || ' CASCADE;';
EXECUTE 'ALTER TABLE #{source}.' || quote_ident(row.tablename) || ' SET SCHEMA #{destination};';
END LOOP;
-- move all <source> views to <destination> schema
FOR row IN SELECT viewname FROM pg_views WHERE schemaname = '#{source}' AND viewowner = '#{owner}'
LOOP
EXECUTE 'DROP VIEW IF EXISTS #{destination}.' || quote_ident(row.viewname) || ' CASCADE;';
EXECUTE 'ALTER VIEW #{source}.' || quote_ident(row.viewname) || ' SET SCHEMA #{destination};';
END LOOP;
END$$;
SQL
end
DatabaseConfiguration = Struct.new(:host, :port, :username, :password, :database)
def self.database_configuration
config = ActiveRecord::Base.connection_pool.db_config.configuration_hash
config = config.with_indifferent_access
# credentials for PostgreSQL in CI environment
if Rails.env.test?
username = ENV["PGUSER"]
password = ENV["PGPASSWORD"]
end
DatabaseConfiguration.new(
config["backup_host"] || config["host"],
config["backup_port"] || config["port"],
config["username"] || username || ENV["USER"] || "postgres",
config["password"] || password,
config["database"]
)
end
private
def self.running_key
"backup_restore_operation_is_running"
end
def self.keep_it_running
# extend the expiry by 1 minute every 30 seconds
Thread.new do
# this thread will be killed when the fork dies
while true
Discourse.redis.expire(running_key, 1.minute)
sleep 30.seconds
end
end
end
def self.shutdown_signal_key
"backup_restore_operation_should_shutdown"
end
def self.set_shutdown_signal!
Discourse.redis.set(shutdown_signal_key, "1")
end
def self.clear_shutdown_signal!
Discourse.redis.del(shutdown_signal_key)
end
def self.save_start_logs_message_id
id = MessageBus.last_id(LOGS_CHANNEL)
Discourse.redis.set(start_logs_message_id_key, id)
end
def self.start_logs_message_id
Discourse.redis.get(start_logs_message_id_key).to_i
end
def self.start_logs_message_id_key
"start_logs_message_id"
end
def self.spawn_process!(type, user_id, opts)
script = File.join(Rails.root, "script", "spawn_backup_restore.rb")
command = ["bundle", "exec", "ruby", script, type, user_id, opts.to_json].map(&:to_s)
pid = spawn({ "RAILS_DB" => RailsMultisite::ConnectionManagement.current_db }, *command)
Process.detach(pid)
end
def self.backup_tables_count
DB.query_single("SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = 'backup'").first.to_i
end
end