Skip to content

Commit 72cdf15

Browse files
authored
Merge pull request rails#44591 from rails/defer-db-connect
Simplify adapter construction; defer connect until first use
2 parents 7fe221d + 91a3a2d commit 72cdf15

18 files changed

+232
-175
lines changed

activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ def commit_db_transaction() end
390390
def rollback_db_transaction
391391
exec_rollback_db_transaction
392392
rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::ConnectionFailed
393-
reconnect!
393+
# Connection's gone; that counts as a rollback
394394
end
395395

396396
def exec_rollback_db_transaction() end # :nodoc:

activerecord/lib/active_record/connection_adapters/abstract/transaction.rb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -414,13 +414,6 @@ def materialize_transactions
414414
@has_unmaterialized_transactions = false
415415
end
416416
end
417-
418-
# As a logical simplification for now, we assume anything that requests
419-
# materialization is about to dirty the transaction. Note this is just
420-
# an assumption about the caller, not a direct property of this method.
421-
# It can go away later when callers are able to handle dirtiness for
422-
# themselves.
423-
dirty_current_transaction
424417
end
425418

426419
def commit_transaction
@@ -446,8 +439,12 @@ def commit_transaction
446439

447440
def rollback_transaction(transaction = nil)
448441
@connection.lock.synchronize do
449-
transaction ||= @stack.pop
450-
transaction.rollback
442+
transaction ||= @stack.last
443+
begin
444+
transaction.rollback
445+
ensure
446+
@stack.pop if @stack.last == transaction
447+
end
451448
transaction.rollback_records
452449
end
453450
end
@@ -502,6 +499,9 @@ def within_new_transaction(isolation: nil, joinable: true)
502499

503500
begin
504501
commit_transaction
502+
rescue ActiveRecord::ConnectionFailed
503+
transaction.state.invalidate! unless transaction.state.completed?
504+
raise
505505
rescue Exception
506506
rollback_transaction(transaction) unless transaction.state.completed?
507507
raise

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -89,34 +89,53 @@ def self.quoted_table_names # :nodoc:
8989
@quoted_table_names ||= {}
9090
end
9191

92-
def initialize(connection, logger = nil, config = {}) # :nodoc:
92+
def initialize(config_or_deprecated_connection, deprecated_logger = nil, deprecated_connection_options = nil, deprecated_config = nil) # :nodoc:
9393
super()
9494

95-
@raw_connection = connection
96-
@owner = nil
97-
@instrumenter = ActiveSupport::Notifications.instrumenter
98-
@logger = logger
99-
@config = config
100-
@pool = ActiveRecord::ConnectionAdapters::NullPool.new
101-
@idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
95+
@raw_connection = nil
96+
@unconfigured_connection = nil
97+
98+
if config_or_deprecated_connection.is_a?(Hash)
99+
@config = config_or_deprecated_connection.symbolize_keys
100+
@logger = ActiveRecord::Base.logger
101+
102+
if deprecated_logger || deprecated_connection_options || deprecated_config
103+
raise ArgumentError, "when initializing an ActiveRecord adapter with a config hash, that should be the only argument"
104+
end
105+
else
106+
# Soft-deprecated for now; we'll probably warn in future.
107+
108+
@unconfigured_connection = config_or_deprecated_connection
109+
@logger = deprecated_logger || ActiveRecord::Base.logger
110+
if deprecated_config
111+
@config = (deprecated_config || {}).symbolize_keys
112+
@connection_parameters = deprecated_connection_options
113+
else
114+
@config = (deprecated_connection_options || {}).symbolize_keys
115+
@connection_parameters = nil
116+
end
117+
end
118+
119+
@owner = nil
120+
@instrumenter = ActiveSupport::Notifications.instrumenter
121+
@pool = ActiveRecord::ConnectionAdapters::NullPool.new
122+
@idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
102123
@visitor = arel_visitor
103124
@statements = build_statement_pool
104125
@lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
105126

106127
@prepared_statements = self.class.type_cast_config_to_boolean(
107-
config.fetch(:prepared_statements, true)
128+
@config.fetch(:prepared_statements, true)
108129
)
109130

110131
@advisory_locks_enabled = self.class.type_cast_config_to_boolean(
111-
config.fetch(:advisory_locks, true)
132+
@config.fetch(:advisory_locks, true)
112133
)
113134

114-
@default_timezone = self.class.validate_default_timezone(config[:default_timezone])
135+
@default_timezone = self.class.validate_default_timezone(@config[:default_timezone])
115136

116137
@raw_connection_dirty = false
117138
@verified = false
118-
119-
configure_connection
120139
end
121140

122141
EXCEPTION_NEVER = { Exception => :never }.freeze # :nodoc:
@@ -147,7 +166,7 @@ def use_metadata_table?
147166
end
148167

149168
def connection_retries
150-
(@config[:connection_retries] || 3).to_i
169+
(@config[:connection_retries] || 1).to_i
151170
end
152171

153172
def default_timezone
@@ -314,7 +333,14 @@ def adapter_name
314333

315334
# Does the database for this adapter exist?
316335
def self.database_exists?(config)
317-
raise NotImplementedError
336+
new(config).database_exists?
337+
end
338+
339+
def database_exists?
340+
connect!
341+
true
342+
rescue ActiveRecord::NoDatabaseError
343+
false
318344
end
319345

320346
# Does this adapter support DDL rollbacks in transactions? That is, would
@@ -598,6 +624,7 @@ def reconnect!(restore_transactions: false)
598624
def disconnect!
599625
clear_cache!(new_connection: true)
600626
reset_transaction
627+
@raw_connection_dirty = false
601628
end
602629

603630
# Immediately forget this connection ever existed. Unlike disconnect!,
@@ -658,10 +685,30 @@ def requires_reloading?
658685
# This is done under the hood by calling #active?. If the connection
659686
# is no longer active, then this method will reconnect to the database.
660687
def verify!
661-
reconnect!(restore_transactions: true) unless active?
688+
unless active?
689+
if @unconfigured_connection
690+
@lock.synchronize do
691+
if @unconfigured_connection
692+
@raw_connection = @unconfigured_connection
693+
@unconfigured_connection = nil
694+
configure_connection
695+
@verified = true
696+
return
697+
end
698+
end
699+
end
700+
701+
reconnect!(restore_transactions: true)
702+
end
703+
662704
@verified = true
663705
end
664706

707+
def connect!
708+
verify!
709+
self
710+
end
711+
665712
def clean! # :nodoc:
666713
@raw_connection_dirty = false
667714
@verified = nil
@@ -864,6 +911,8 @@ def reconnect_can_restore_state?
864911
#
865912
def with_raw_connection(allow_retry: false, uses_transaction: true)
866913
@lock.synchronize do
914+
connect! if @raw_connection.nil? && reconnect_can_restore_state?
915+
867916
materialize_transactions if uses_transaction
868917

869918
retries_available = allow_retry ? connection_retries : 0
@@ -909,6 +958,13 @@ def with_raw_connection(allow_retry: false, uses_transaction: true)
909958
end
910959
end
911960

961+
unless retryable_query_error?(translated_exception)
962+
# Barring a known-retryable error inside the query (regardless of
963+
# whether we were in a _position_ to retry it), we should infer that
964+
# there's likely a real problem with the connection.
965+
@verified = false
966+
end
967+
912968
raise translated_exception
913969
ensure
914970
dirty_current_transaction if uses_transaction
@@ -942,7 +998,7 @@ def reconnect
942998
# to both be thread-safe and not rely upon actual server communication.
943999
# This is useful for e.g. string escaping methods.
9441000
def any_raw_connection
945-
@raw_connection
1001+
@raw_connection || valid_raw_connection
9461002
end
9471003

9481004
# Similar to any_raw_connection, but ensures it is validated and

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,6 @@ def dealloc(stmt)
5151
end
5252
end
5353

54-
def initialize(connection, logger, connection_options, config)
55-
super(connection, logger, config)
56-
end
57-
5854
def get_database_version # :nodoc:
5955
full_version_string = get_full_version
6056
version_string = version_string(full_version_string)

activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb

Lines changed: 25 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,7 @@ module ActiveRecord
1010
module ConnectionHandling # :nodoc:
1111
# Establishes a connection to the database that's used by all Active Record objects.
1212
def mysql2_connection(config)
13-
config = config.symbolize_keys
14-
config[:flags] ||= 0
15-
16-
if config[:flags].kind_of? Array
17-
config[:flags].push "FOUND_ROWS"
18-
else
19-
config[:flags] |= Mysql2::Client::FOUND_ROWS
20-
end
21-
22-
ConnectionAdapters::Mysql2Adapter.new(
23-
ConnectionAdapters::Mysql2Adapter.new_client(config),
24-
logger,
25-
nil,
26-
config,
27-
)
13+
ConnectionAdapters::Mysql2Adapter.new(config)
2814
end
2915
end
3016

@@ -55,16 +41,25 @@ def new_client(config)
5541
end
5642
end
5743

58-
def initialize(connection, logger, connection_options, config)
59-
check_prepared_statements_deprecation(config)
60-
superclass_config = config.reverse_merge(prepared_statements: false)
61-
super(connection, logger, connection_options, superclass_config)
62-
end
44+
def initialize(...)
45+
super
46+
47+
@config[:flags] ||= 0
6348

64-
def self.database_exists?(config)
65-
!!ActiveRecord::Base.mysql2_connection(config)
66-
rescue ActiveRecord::NoDatabaseError
67-
false
49+
if @config[:flags].kind_of? Array
50+
@config[:flags].push "FOUND_ROWS"
51+
else
52+
@config[:flags] |= Mysql2::Client::FOUND_ROWS
53+
end
54+
55+
unless @config.key?(:prepared_statements)
56+
ActiveSupport::Deprecation.warn(<<-MSG.squish)
57+
The default value of `prepared_statements` for the mysql2 adapter will be changed from +false+ to +true+ in Rails 7.2.
58+
MSG
59+
@config[:prepared_statements] = false
60+
end
61+
62+
@connection_parameters ||= @config
6863
end
6964

7065
def supports_json?
@@ -120,7 +115,7 @@ def quote_string(string)
120115
#++
121116

122117
def active?
123-
@raw_connection.ping
118+
!!@raw_connection&.ping
124119
end
125120

126121
alias :reset! :reconnect!
@@ -129,30 +124,23 @@ def active?
129124
# Otherwise, this method does nothing.
130125
def disconnect!
131126
super
132-
@raw_connection.close
127+
@raw_connection&.close
128+
@raw_connection = nil
133129
end
134130

135131
def discard! # :nodoc:
136132
super
137-
@raw_connection.automatic_close = false
133+
@raw_connection&.automatic_close = false
138134
@raw_connection = nil
139135
end
140136

141137
private
142-
def check_prepared_statements_deprecation(config)
143-
if !config.key?(:prepared_statements)
144-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
145-
The default value of `prepared_statements` for the mysql2 adapter will be changed from +false+ to +true+ in Rails 7.2.
146-
MSG
147-
end
148-
end
149-
150138
def connect
151-
@raw_connection = self.class.new_client(@config)
139+
@raw_connection = self.class.new_client(@connection_parameters)
152140
end
153141

154142
def reconnect
155-
@raw_connection.close
143+
@raw_connection&.close
156144
connect
157145
end
158146

0 commit comments

Comments
 (0)