diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index bacb9d714e..d2bece5303 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,9 @@ +* Raise specific exception when a connection is not defined. + + The new `ConnectionNotDefined` exception provides connection name, shard and role accessors indicating the details of the connection that was requested. + + *Hana Harencarova*, *Matthew Draper* + * Delete the deprecated constant `ActiveRecord::ImmutableRelation`. *Xavier Noria* diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_handler.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_handler.rb index c1962812f8..be5006fc21 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_handler.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_handler.rb @@ -210,18 +210,25 @@ def remove_connection_pool(connection_name, role: ActiveRecord::Base.current_rol # This makes retrieving the connection pool O(1) once the process is warm. # When a connection is established or removed, we invalidate the cache. def retrieve_connection_pool(connection_name, role: ActiveRecord::Base.current_role, shard: ActiveRecord::Base.current_shard, strict: false) - pool = get_pool_manager(connection_name)&.get_pool_config(role, shard)&.pool + pool_manager = get_pool_manager(connection_name) + pool = pool_manager&.get_pool_config(role, shard)&.pool if strict && !pool - if shard != ActiveRecord::Base.default_shard - message = "No connection pool for '#{connection_name}' found for the '#{shard}' shard." - elsif role != ActiveRecord::Base.default_role - message = "No connection pool for '#{connection_name}' found for the '#{role}' role." - else - message = "No connection pool for '#{connection_name}' found." - end + selector = [ + ("'#{shard}' shard" unless shard == ActiveRecord::Base.default_shard), + ("'#{role}' role" unless role == ActiveRecord::Base.default_role), + ].compact.join(" and ") - raise ConnectionNotEstablished, message + selector = [ + (connection_name unless connection_name == "ActiveRecord::Base"), + selector.presence, + ].compact.join(" with ") + + selector = " for #{selector}" if selector.present? + + message = "No database connection defined#{selector}." + + raise ConnectionNotDefined.new(message, connection_name: connection_name, shard: shard, role: role) end pool diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 38044dcf48..a4b6793999 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1044,7 +1044,8 @@ def verified! end def retryable_connection_error?(exception) - exception.is_a?(ConnectionNotEstablished) || exception.is_a?(ConnectionFailed) + (exception.is_a?(ConnectionNotEstablished) && !exception.is_a?(ConnectionNotDefined)) || + exception.is_a?(ConnectionFailed) end def invalidate_transaction(exception) diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index bc039f19aa..35000cb5c9 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -84,6 +84,19 @@ def set_pool(connection_pool) class ConnectionTimeoutError < ConnectionNotEstablished end + # Raised when a database connection pool is requested but + # has not been defined. + class ConnectionNotDefined < ConnectionNotEstablished + def initialize(message = nil, connection_name: nil, role: nil, shard: nil) + super(message) + @connection_name = connection_name + @role = role + @shard = shard + end + + attr_reader :connection_name, :role, :shard + end + # Raised when connection to the database could not been established because it was not # able to connect to the host or when the authorization failed. class DatabaseConnectionError < ConnectionNotEstablished diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb index 7b95129fc9..98c682159f 100644 --- a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb @@ -359,13 +359,13 @@ def test_retrieve_connection_pool_with_invalid_id end def test_calling_connected_to_on_a_non_existent_handler_raises - error = assert_raises ActiveRecord::ConnectionNotEstablished do + error = assert_raises ActiveRecord::ConnectionNotDefined do ActiveRecord::Base.connected_to(role: :non_existent) do Person.first end end - assert_equal "No connection pool for 'ActiveRecord::Base' found for the 'non_existent' role.", error.message + assert_equal "No database connection defined for 'non_existent' role.", error.message end def test_default_handlers_are_writing_and_reading diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_sharding_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_sharding_db_test.rb index 702bef1096..3088f7118b 100644 --- a/activerecord/test/cases/connection_adapters/connection_handlers_sharding_db_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handlers_sharding_db_test.rb @@ -21,7 +21,7 @@ def test_establishing_a_connection_in_connected_to_block_uses_current_role_and_s ActiveRecord::Base.establish_connection(db_config) assert_nothing_raised { Person.first } - assert_equal [:default, :shard_one], ActiveRecord::Base.connection_handler.send(:get_pool_manager, "ActiveRecord::Base").shard_names + assert_equal [:default, :shard_one].sort, ActiveRecord::Base.connection_handler.send(:get_pool_manager, "ActiveRecord::Base").shard_names.sort end end @@ -233,13 +233,51 @@ def test_calling_connected_to_on_a_non_existent_shard_raises default: { writing: :arunit, reading: :arunit } }) - error = assert_raises ActiveRecord::ConnectionNotEstablished do + error = assert_raises ActiveRecord::ConnectionNotDefined do ActiveRecord::Base.connected_to(role: :reading, shard: :foo) do Person.first end end - assert_equal "No connection pool for 'ActiveRecord::Base' found for the 'foo' shard.", error.message + assert_equal "No database connection defined for 'foo' shard and 'reading' role.", error.message + assert_equal "ActiveRecord::Base", error.connection_name + assert_equal :foo, error.shard + assert_equal :reading, error.role + end + + def test_calling_connected_to_on_a_non_existent_role_for_shard_raises + ActiveRecord::Base.connects_to(shards: { + default: { writing: :arunit, reading: :arunit }, + shard_one: { writing: :arunit, reading: :arunit } + }) + + error = assert_raises ActiveRecord::ConnectionNotDefined do + ActiveRecord::Base.connected_to(role: :non_existent, shard: :shard_one) do + Person.first + end + end + + assert_equal "No database connection defined for 'shard_one' shard and 'non_existent' role.", error.message + assert_equal "ActiveRecord::Base", error.connection_name + assert_equal :shard_one, error.shard + assert_equal :non_existent, error.role + end + + def test_calling_connected_to_on_a_default_role_for_non_existent_shard_raises + ActiveRecord::Base.connects_to(shards: { + default: { writing: :arunit, reading: :arunit } + }) + + error = assert_raises ActiveRecord::ConnectionNotDefined do + ActiveRecord::Base.connected_to(shard: :foo) do + Person.first + end + end + + assert_equal "No database connection defined for 'foo' shard.", error.message + assert_equal "ActiveRecord::Base", error.connection_name + assert_equal :foo, error.shard + assert_equal :writing, error.role end def test_cannot_swap_shards_while_prohibited diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 57ef208c45..afcb520caa 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -1710,7 +1710,7 @@ def test_only_existing_connections_are_replaced setup_shared_connection_pool - assert_raises(ActiveRecord::ConnectionNotEstablished) do + assert_raises(ActiveRecord::ConnectionNotDefined) do ActiveRecord::Base.connected_to(role: :reading, shard: :two) do ActiveRecord::Base.retrieve_connection end @@ -1721,7 +1721,7 @@ def test_only_existing_connections_are_restored clean_up_connection_handler teardown_shared_connection_pool - assert_raises(ActiveRecord::ConnectionNotEstablished) do + assert_raises(ActiveRecord::ConnectionNotDefined) do ActiveRecord::Base.connected_to(role: :reading) do ActiveRecord::Base.retrieve_connection end diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb index 14a4d63f57..c74032a0ea 100644 --- a/activerecord/test/cases/unconnected_test.rb +++ b/activerecord/test/cases/unconnected_test.rb @@ -23,21 +23,21 @@ def setup end def test_connection_no_longer_established - assert_raise(ActiveRecord::ConnectionNotEstablished) do + assert_raise(ActiveRecord::ConnectionNotDefined) do TestRecord.find(1) end - assert_raise(ActiveRecord::ConnectionNotEstablished) do + assert_raise(ActiveRecord::ConnectionNotDefined) do TestRecord.new.save end end def test_error_message_when_connection_not_established - error = assert_raise(ActiveRecord::ConnectionNotEstablished) do + error = assert_raise(ActiveRecord::ConnectionNotDefined) do TestRecord.find(1) end - assert_equal "No connection pool for 'ActiveRecord::Base' found.", error.message + assert_equal "No database connection defined.", error.message end def test_underlying_adapter_no_longer_active