diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 673c7b9156..53caec00bb 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -146,7 +146,7 @@ To keep using the current cache store, you can turn off cache versioning entirel current_version = begin ActiveRecord::Migrator.current_version rescue ActiveRecordError => error - warn "Failed to load the schema cache because of #{error.class}: #{error.message}" + warn "Failed to validate the schema cache because of #{error.class}: #{error.message}" nil end next if current_version.nil? @@ -167,16 +167,29 @@ To keep using the current cache store, you can turn off cache versioning entirel config.after_initialize do ActiveSupport.on_load(:active_record) do if app.config.eager_load - descendants.each do |model| - # SchemaMigration and InternalMetadata both override `table_exists?` - # to bypass the schema cache, so skip them to avoid the extra queries. - next if model._internal? + begin + descendants.each do |model| + # SchemaMigration and InternalMetadata both override `table_exists?` + # to bypass the schema cache, so skip them to avoid the extra queries. + next if model._internal? - # If there's no connection yet, or the schema cache doesn't have the columns - # hash for the model cached, `define_attribute_methods` would trigger a query. - next unless model.connected? && model.connection.schema_cache.columns_hash?(model.table_name) + # If the schema cache was loaded from a dump, we can use it without connecting + schema_cache = model.connection_pool.schema_cache - model.define_attribute_methods + # If there's no connection yet, we avoid connecting. + schema_cache ||= model.connected? && model.connection.schema_cache + + # If the schema cache doesn't have the columns + # hash for the model cached, `define_attribute_methods` would trigger a query. + if schema_cache && schema_cache.columns_hash?(model.table_name) + model.define_attribute_methods + end + end + rescue ActiveRecordError => error + # Regardless of wether there was already a connection or not, we rescue any database + # error because it is critical that the application can boot even if the database + # is unhealthy. + warn "Failed to define attribute methods because of #{error.class}: #{error.message}" end end end diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 5bb6d205cd..624caacc5b 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true require "isolation/abstract_unit" +require "env_helpers" module ApplicationTests class FrameworksTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation + include EnvHelpers def setup build_app @@ -214,10 +216,24 @@ module ApplicationTests assert !defined?(ActiveRecord::Base) || ActiveRecord.autoload?(:Base) end + test "can boot with an unhealthy database" do + rails %w(generate model post title:string) + + switch_env("DATABASE_URL", "mysql2://127.0.0.1:1") do + require "#{app_path}/config/environment" + end + end + test "use schema cache dump" do rails %w(generate model post title:string) rails %w(db:migrate db:schema:cache:dump) + + add_to_config <<-RUBY + config.eager_load = true + RUBY + require "#{app_path}/config/environment" + assert ActiveRecord::Base.connection.schema_cache.data_sources("posts") ensure ActiveRecord::Base.connection.drop_table("posts", if_exists: true) # force drop posts table for test. @@ -226,20 +242,65 @@ module ApplicationTests test "expire schema cache dump" do rails %w(generate model post title:string) rails %w(db:migrate db:schema:cache:dump db:rollback) + + add_to_config <<-RUBY + config.eager_load = true + RUBY + require "#{app_path}/config/environment" assert_not ActiveRecord::Base.connection.schema_cache.data_sources("posts") end - test "does not expire schema cache dump if check_schema_cache_dump_version is false" do + test "expire schema cache dump if the version can't be checked because the database is unhealthy" do + rails %w(generate model post title:string) + rails %w(db:migrate db:schema:cache:dump) + add_to_config <<-RUBY - config.active_record.check_schema_cache_dump_version = false + config.eager_load = true RUBY + + switch_env("DATABASE_URL", "mysql2://127.0.0.1:1") do + require "#{app_path}/config/environment" + + assert_nil ActiveRecord::Base.connection_pool.schema_cache + assert_raises ActiveRecord::ConnectionNotEstablished do + ActiveRecord::Base.connection.execute("SELECT 1") + end + end + end + + test "does not expire schema cache dump if check_schema_cache_dump_version is false" do rails %w(generate model post title:string) rails %w(db:migrate db:schema:cache:dump db:rollback) + + add_to_config <<-RUBY + config.eager_load = true + config.active_record.check_schema_cache_dump_version = false + RUBY + require "#{app_path}/config/environment" assert ActiveRecord::Base.connection_pool.schema_cache.data_sources("posts") end + test "does not expire schema cache dump if check_schema_cache_dump_version is false and the database unhealthy" do + rails %w(generate model post title:string) + rails %w(db:migrate db:schema:cache:dump db:rollback) + + add_to_config <<-RUBY + config.eager_load = true + config.active_record.check_schema_cache_dump_version = false + RUBY + + switch_env("DATABASE_URL", "mysql2://127.0.0.1:1") do + require "#{app_path}/config/environment" + + assert ActiveRecord::Base.connection_pool.schema_cache.data_sources("posts") + assert_raises ActiveRecord::ConnectionNotEstablished do + ActiveRecord::Base.connection.execute("SELECT 1") + end + end + end + test "active record establish_connection uses Rails.env if DATABASE_URL is not set" do require "#{app_path}/config/environment" orig_database_url = ENV.delete("DATABASE_URL")