mirror of
https://github.com/discourse/discourse.git
synced 2024-11-24 09:17:30 +08:00
45ccadeeeb
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.
186 lines
5.1 KiB
Ruby
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
|