mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 10:20:58 -06:00
ac70c48be4
After restoring a backup it takes up to 48 hours for uploads stored on S3 to appear in the S3 inventory. This change prevents alerts about missing uploads by preventing the EnsureS3UploadsExistence job from running in the first 48 hours after a restore. During the restore it deletes the count of missing uploads from the PluginStore, so that an alert isn't triggered by an old number.
276 lines
9.7 KiB
Ruby
276 lines
9.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
require_relative 'shared_context_for_backup_restore'
|
|
|
|
describe BackupRestore::DatabaseRestorer do
|
|
include_context "shared stuff"
|
|
|
|
let(:current_db) { RailsMultisite::ConnectionManagement.current_db }
|
|
subject { BackupRestore::DatabaseRestorer.new(logger, current_db) }
|
|
|
|
def expect_create_readonly_functions
|
|
Migration::BaseDropper.expects(:create_readonly_function).at_least_once
|
|
end
|
|
|
|
def expect_table_move
|
|
BackupRestore.expects(:move_tables_between_schemas).with("public", "backup").once
|
|
end
|
|
|
|
def expect_psql(output_lines: ["output from psql"], exit_status: 0, stub_thread: false)
|
|
status = mock("psql status")
|
|
status.expects(:exitstatus).returns(exit_status).once
|
|
Process.expects(:last_status).returns(status).once
|
|
|
|
if stub_thread
|
|
thread = mock("thread")
|
|
thread.stubs(:join)
|
|
Thread.stubs(:new).returns(thread)
|
|
end
|
|
|
|
output_lines << nil
|
|
psql_io = mock("psql")
|
|
psql_io.expects(:readline).returns(*output_lines).times(output_lines.size)
|
|
IO.expects(:popen).yields(psql_io).once
|
|
end
|
|
|
|
def expect_db_migrate
|
|
Discourse::Utils.expects(:execute_command).with do |env, command, options|
|
|
env["SKIP_POST_DEPLOYMENT_MIGRATIONS"] == "0" &&
|
|
env["SKIP_OPTIMIZE_ICONS"] == "1" &&
|
|
env["DISABLE_TRANSLATION_OVERRIDES"] == "1" &&
|
|
command == "rake db:migrate" &&
|
|
options[:chdir] == Rails.root
|
|
end.once
|
|
end
|
|
|
|
def expect_db_reconnect
|
|
RailsMultisite::ConnectionManagement.expects(:establish_connection).once
|
|
end
|
|
|
|
def execute_stubbed_restore(stub_readonly_functions: true, stub_psql: true, stub_migrate: true,
|
|
dump_file_path: "foo.sql")
|
|
expect_table_move
|
|
expect_create_readonly_functions if stub_readonly_functions
|
|
expect_psql if stub_psql
|
|
expect_db_migrate if stub_migrate
|
|
subject.restore(dump_file_path)
|
|
end
|
|
|
|
describe "#restore" do
|
|
it "executes everything in the correct order" do
|
|
restore = sequence("restore")
|
|
expect_table_move.in_sequence(restore)
|
|
expect_create_readonly_functions.in_sequence(restore)
|
|
expect_psql(stub_thread: true).in_sequence(restore)
|
|
expect_db_migrate.in_sequence(restore)
|
|
expect_db_reconnect.in_sequence(restore)
|
|
|
|
subject.restore("foo.sql")
|
|
end
|
|
|
|
it "stores the date of the last restore" do
|
|
date_string = "2020-01-10T17:38:27Z"
|
|
freeze_time(Time.parse(date_string))
|
|
execute_stubbed_restore
|
|
|
|
expect(BackupMetadata.value_for(BackupMetadata::LAST_RESTORE_DATE)).to eq(date_string)
|
|
end
|
|
|
|
context "with real psql" do
|
|
after do
|
|
psql = BackupRestore::DatabaseRestorer.psql_command
|
|
system("#{psql} -c 'DROP TABLE IF EXISTS foo'", [:out, :err] => File::NULL)
|
|
end
|
|
|
|
def restore(filename, stub_migrate: true)
|
|
path = File.join(Rails.root, "spec/fixtures/db/restore", filename)
|
|
execute_stubbed_restore(stub_psql: false, stub_migrate: stub_migrate, dump_file_path: path)
|
|
end
|
|
|
|
def expect_restore_to_work(filename)
|
|
restore(filename, stub_migrate: true)
|
|
expect(ActiveRecord::Base.connection.table_exists?("foo")).to eq(true)
|
|
end
|
|
|
|
it "restores from PostgreSQL 9.3" do
|
|
# this covers the defaults of Discourse v1.0 up to v1.5
|
|
expect_restore_to_work("postgresql_9.3.11.sql")
|
|
end
|
|
|
|
it "restores from PostgreSQL 9.5.5" do
|
|
# it uses a slightly different header than later 9.5.x versions
|
|
expect_restore_to_work("postgresql_9.5.5.sql")
|
|
end
|
|
|
|
it "restores from PostgreSQL 9.5" do
|
|
# this covers the defaults of Discourse v1.6 up to v1.9
|
|
expect_restore_to_work("postgresql_9.5.10.sql")
|
|
end
|
|
|
|
it "restores from PostgreSQL 10" do
|
|
# this covers the defaults of Discourse v1.7 up to v2.4
|
|
expect_restore_to_work("postgresql_10.11.sql")
|
|
end
|
|
|
|
it "restores from PostgreSQL 11" do
|
|
expect_restore_to_work("postgresql_11.6.sql")
|
|
end
|
|
|
|
it "restores from PostgreSQL 12" do
|
|
expect_restore_to_work("postgresql_12.1.sql")
|
|
end
|
|
|
|
it "detects error during restore" do
|
|
expect { restore("error.sql", stub_migrate: false) }
|
|
.to raise_error(BackupRestore::DatabaseRestoreError)
|
|
end
|
|
end
|
|
|
|
context "rewrites database dump" do
|
|
let(:logger) do
|
|
Class.new do
|
|
attr_reader :log_messages
|
|
|
|
def initialize
|
|
@log_messages = []
|
|
end
|
|
|
|
def log(message, ex = nil)
|
|
@log_messages << message if message
|
|
end
|
|
end.new
|
|
end
|
|
|
|
def restore_and_log_output(filename)
|
|
path = File.join(Rails.root, "spec/fixtures/db/restore", filename)
|
|
BackupRestore::DatabaseRestorer.stubs(:psql_command).returns("cat")
|
|
execute_stubbed_restore(stub_psql: false, dump_file_path: path)
|
|
logger.log_messages.join("\n")
|
|
end
|
|
|
|
it "replaces `EXECUTE FUNCTION` when restoring on PostgreSQL < 11" do
|
|
BackupRestore.stubs(:postgresql_major_version).returns(10)
|
|
log = restore_and_log_output("trigger.sql")
|
|
|
|
expect(log).not_to be_blank
|
|
expect(log).not_to match(/CREATE SCHEMA public/)
|
|
expect(log).not_to match(/EXECUTE FUNCTION/)
|
|
expect(log).to match(/^CREATE TRIGGER foo_topic_id_readonly .+? EXECUTE PROCEDURE discourse_functions.raise_foo_topic_id_readonly/)
|
|
expect(log).to match(/^CREATE TRIGGER foo_user_id_readonly .+? EXECUTE PROCEDURE discourse_functions.raise_foo_user_id_readonly/)
|
|
end
|
|
|
|
it "does not replace `EXECUTE FUNCTION` when restoring on PostgreSQL >= 11" do
|
|
BackupRestore.stubs(:postgresql_major_version).returns(11)
|
|
log = restore_and_log_output("trigger.sql")
|
|
|
|
expect(log).not_to be_blank
|
|
expect(log).not_to match(/CREATE SCHEMA public/)
|
|
expect(log).not_to match(/EXECUTE PROCEDURE/)
|
|
expect(log).to match(/^CREATE TRIGGER foo_topic_id_readonly .+? EXECUTE FUNCTION discourse_functions.raise_foo_topic_id_readonly/)
|
|
expect(log).to match(/^CREATE TRIGGER foo_user_id_readonly .+? EXECUTE FUNCTION discourse_functions.raise_foo_user_id_readonly/)
|
|
end
|
|
end
|
|
|
|
context "database connection" do
|
|
it 'reconnects to the correct database', type: :multisite do
|
|
RailsMultisite::ConnectionManagement.establish_connection(db: 'second')
|
|
execute_stubbed_restore
|
|
expect(RailsMultisite::ConnectionManagement.current_db).to eq('second')
|
|
end
|
|
|
|
it 'it is not erroring for non-multisite' do
|
|
expect { execute_stubbed_restore }.not_to raise_error
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#rollback" do
|
|
it "moves tables back when tables were moved" do
|
|
BackupRestore.stubs(:can_rollback?).returns(true)
|
|
BackupRestore.expects(:move_tables_between_schemas).with("backup", "public").never
|
|
subject.rollback
|
|
|
|
execute_stubbed_restore
|
|
|
|
BackupRestore.expects(:move_tables_between_schemas).with("backup", "public").once
|
|
subject.rollback
|
|
end
|
|
end
|
|
|
|
context "readonly functions" do
|
|
before do
|
|
Migration::SafeMigrate.stubs(:post_migration_path).returns("spec/fixtures/db/post_migrate/drop_column")
|
|
end
|
|
|
|
it "doesn't try to drop function when no functions have been created" do
|
|
Migration::BaseDropper.expects(:drop_readonly_function).never
|
|
subject.clean_up
|
|
end
|
|
|
|
it "creates and drops all functions when none exist" do
|
|
Migration::BaseDropper.expects(:create_readonly_function).with(:posts, :via_email)
|
|
Migration::BaseDropper.expects(:create_readonly_function).with(:posts, :raw_email)
|
|
execute_stubbed_restore(stub_readonly_functions: false)
|
|
|
|
Migration::BaseDropper.expects(:drop_readonly_function).with(:posts, :via_email)
|
|
Migration::BaseDropper.expects(:drop_readonly_function).with(:posts, :raw_email)
|
|
subject.clean_up
|
|
end
|
|
|
|
it "creates and drops only missing functions during restore" do
|
|
Migration::BaseDropper.stubs(:existing_discourse_function_names)
|
|
.returns(%w(raise_email_logs_readonly raise_posts_raw_email_readonly))
|
|
|
|
Migration::BaseDropper.expects(:create_readonly_function).with(:posts, :via_email)
|
|
execute_stubbed_restore(stub_readonly_functions: false)
|
|
|
|
Migration::BaseDropper.expects(:drop_readonly_function).with(:posts, :via_email)
|
|
subject.clean_up
|
|
end
|
|
end
|
|
|
|
describe ".drop_backup_schema" do
|
|
subject { BackupRestore::DatabaseRestorer }
|
|
|
|
context "when no backup schema exists" do
|
|
it "doesn't do anything" do
|
|
ActiveRecord::Base.connection.expects(:schema_exists?).with("backup").returns(false)
|
|
ActiveRecord::Base.connection.expects(:drop_schema).never
|
|
|
|
subject.drop_backup_schema
|
|
end
|
|
end
|
|
|
|
context "when a backup schema exists" do
|
|
before do
|
|
ActiveRecord::Base.connection.expects(:schema_exists?).with("backup").returns(true)
|
|
end
|
|
|
|
it "drops the schema when the last restore was long ago" do
|
|
ActiveRecord::Base.connection.expects(:drop_schema).with("backup")
|
|
BackupMetadata.update_last_restore_date(8.days.ago)
|
|
|
|
subject.drop_backup_schema
|
|
end
|
|
|
|
it "doesn't drop the schema when the last restore was recently" do
|
|
ActiveRecord::Base.connection.expects(:drop_schema).with("backup").never
|
|
BackupMetadata.update_last_restore_date(6.days.ago)
|
|
|
|
subject.drop_backup_schema
|
|
end
|
|
|
|
it "stores the current date when there is no record of the last restore" do
|
|
ActiveRecord::Base.connection.expects(:drop_schema).with("backup").never
|
|
|
|
date_string = "2020-01-08T17:38:27Z"
|
|
freeze_time(Time.parse(date_string))
|
|
|
|
subject.drop_backup_schema
|
|
expect(BackupMetadata.value_for(BackupMetadata::LAST_RESTORE_DATE)).to eq(date_string)
|
|
end
|
|
end
|
|
end
|
|
end
|