activerecord 7.1.5.2 → 8.1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +567 -2443
  3. data/README.rdoc +16 -16
  4. data/examples/performance.rb +2 -2
  5. data/lib/active_record/association_relation.rb +3 -2
  6. data/lib/active_record/associations/alias_tracker.rb +31 -23
  7. data/lib/active_record/associations/association.rb +43 -12
  8. data/lib/active_record/associations/belongs_to_association.rb +31 -8
  9. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
  10. data/lib/active_record/associations/builder/association.rb +23 -11
  11. data/lib/active_record/associations/builder/belongs_to.rb +18 -4
  12. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  13. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +2 -2
  14. data/lib/active_record/associations/builder/has_many.rb +3 -4
  15. data/lib/active_record/associations/builder/has_one.rb +3 -4
  16. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  17. data/lib/active_record/associations/collection_association.rb +17 -9
  18. data/lib/active_record/associations/collection_proxy.rb +36 -5
  19. data/lib/active_record/associations/deprecation.rb +88 -0
  20. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  21. data/lib/active_record/associations/errors.rb +268 -0
  22. data/lib/active_record/associations/has_many_association.rb +1 -1
  23. data/lib/active_record/associations/has_many_through_association.rb +10 -3
  24. data/lib/active_record/associations/join_dependency/join_association.rb +1 -1
  25. data/lib/active_record/associations/join_dependency.rb +4 -2
  26. data/lib/active_record/associations/nested_error.rb +47 -0
  27. data/lib/active_record/associations/preloader/association.rb +4 -3
  28. data/lib/active_record/associations/preloader/batch.rb +7 -1
  29. data/lib/active_record/associations/preloader/branch.rb +8 -1
  30. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  31. data/lib/active_record/associations/singular_association.rb +14 -3
  32. data/lib/active_record/associations/through_association.rb +1 -1
  33. data/lib/active_record/associations.rb +250 -315
  34. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  35. data/lib/active_record/attribute_assignment.rb +0 -2
  36. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  37. data/lib/active_record/attribute_methods/primary_key.rb +25 -61
  38. data/lib/active_record/attribute_methods/query.rb +34 -0
  39. data/lib/active_record/attribute_methods/read.rb +1 -13
  40. data/lib/active_record/attribute_methods/serialization.rb +20 -27
  41. data/lib/active_record/attribute_methods/time_zone_conversion.rb +19 -20
  42. data/lib/active_record/attribute_methods.rb +71 -75
  43. data/lib/active_record/attributes.rb +96 -68
  44. data/lib/active_record/autosave_association.rb +93 -58
  45. data/lib/active_record/base.rb +5 -7
  46. data/lib/active_record/callbacks.rb +1 -1
  47. data/lib/active_record/coders/json.rb +14 -5
  48. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +50 -126
  49. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -4
  50. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +52 -13
  51. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +681 -135
  52. data/lib/active_record/connection_adapters/abstract/database_statements.rb +165 -86
  53. data/lib/active_record/connection_adapters/abstract/query_cache.rb +226 -78
  54. data/lib/active_record/connection_adapters/abstract/quoting.rb +79 -114
  55. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +11 -7
  56. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +37 -36
  57. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  58. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +136 -34
  59. data/lib/active_record/connection_adapters/abstract/transaction.rb +163 -68
  60. data/lib/active_record/connection_adapters/abstract_adapter.rb +197 -129
  61. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +112 -63
  62. data/lib/active_record/connection_adapters/column.rb +17 -4
  63. data/lib/active_record/connection_adapters/mysql/database_statements.rb +11 -3
  64. data/lib/active_record/connection_adapters/mysql/quoting.rb +50 -57
  65. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  66. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +41 -10
  67. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +79 -46
  68. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +97 -101
  69. data/lib/active_record/connection_adapters/mysql2_adapter.rb +14 -33
  70. data/lib/active_record/connection_adapters/pool_config.rb +14 -13
  71. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -0
  72. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +98 -44
  73. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +3 -3
  74. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  75. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  76. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  77. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  78. data/lib/active_record/connection_adapters/postgresql/quoting.rb +79 -68
  79. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  80. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +9 -17
  81. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +44 -41
  82. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +69 -32
  83. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +155 -75
  84. data/lib/active_record/connection_adapters/postgresql_adapter.rb +104 -121
  85. data/lib/active_record/connection_adapters/schema_cache.rb +126 -133
  86. data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
  87. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +91 -95
  88. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +57 -54
  89. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +16 -0
  90. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
  91. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +43 -2
  92. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +38 -15
  93. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +231 -111
  94. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  95. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +40 -69
  96. data/lib/active_record/connection_adapters/trilogy_adapter.rb +19 -65
  97. data/lib/active_record/connection_adapters.rb +66 -0
  98. data/lib/active_record/connection_handling.rb +88 -46
  99. data/lib/active_record/core.rb +142 -58
  100. data/lib/active_record/counter_cache.rb +52 -18
  101. data/lib/active_record/database_configurations/connection_url_resolver.rb +9 -2
  102. data/lib/active_record/database_configurations/database_config.rb +28 -5
  103. data/lib/active_record/database_configurations/hash_config.rb +103 -41
  104. data/lib/active_record/database_configurations/url_config.rb +30 -1
  105. data/lib/active_record/database_configurations.rb +8 -4
  106. data/lib/active_record/delegated_type.rb +43 -19
  107. data/lib/active_record/dynamic_matchers.rb +56 -71
  108. data/lib/active_record/encryption/config.rb +3 -1
  109. data/lib/active_record/encryption/encryptable_record.rb +12 -12
  110. data/lib/active_record/encryption/encrypted_attribute_type.rb +35 -6
  111. data/lib/active_record/encryption/encryptor.rb +58 -22
  112. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  113. data/lib/active_record/encryption/key_provider.rb +1 -1
  114. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  115. data/lib/active_record/encryption/message_serializer.rb +4 -0
  116. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  117. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  118. data/lib/active_record/encryption/scheme.rb +9 -2
  119. data/lib/active_record/enum.rb +57 -36
  120. data/lib/active_record/errors.rb +76 -26
  121. data/lib/active_record/explain.rb +14 -25
  122. data/lib/active_record/explain_registry.rb +51 -2
  123. data/lib/active_record/filter_attribute_handler.rb +73 -0
  124. data/lib/active_record/fixture_set/table_row.rb +19 -2
  125. data/lib/active_record/fixtures.rb +39 -35
  126. data/lib/active_record/future_result.rb +21 -13
  127. data/lib/active_record/gem_version.rb +3 -3
  128. data/lib/active_record/inheritance.rb +5 -3
  129. data/lib/active_record/insert_all.rb +28 -20
  130. data/lib/active_record/integration.rb +4 -1
  131. data/lib/active_record/internal_metadata.rb +48 -34
  132. data/lib/active_record/locking/optimistic.rb +15 -7
  133. data/lib/active_record/locking/pessimistic.rb +5 -0
  134. data/lib/active_record/log_subscriber.rb +3 -34
  135. data/lib/active_record/message_pack.rb +1 -1
  136. data/lib/active_record/middleware/shard_selector.rb +34 -17
  137. data/lib/active_record/migration/command_recorder.rb +46 -14
  138. data/lib/active_record/migration/compatibility.rb +40 -25
  139. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  140. data/lib/active_record/migration/default_strategy.rb +4 -5
  141. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  142. data/lib/active_record/migration.rb +119 -103
  143. data/lib/active_record/model_schema.rb +66 -78
  144. data/lib/active_record/nested_attributes.rb +17 -9
  145. data/lib/active_record/persistence.rb +161 -454
  146. data/lib/active_record/query_cache.rb +33 -15
  147. data/lib/active_record/query_logs.rb +106 -39
  148. data/lib/active_record/query_logs_formatter.rb +17 -28
  149. data/lib/active_record/querying.rb +28 -16
  150. data/lib/active_record/railtie.rb +61 -74
  151. data/lib/active_record/railties/controller_runtime.rb +19 -5
  152. data/lib/active_record/railties/databases.rake +64 -78
  153. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  154. data/lib/active_record/railties/job_runtime.rb +10 -11
  155. data/lib/active_record/reflection.rb +147 -53
  156. data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
  157. data/lib/active_record/relation/batches.rb +153 -73
  158. data/lib/active_record/relation/calculations.rb +146 -96
  159. data/lib/active_record/relation/delegation.rb +30 -23
  160. data/lib/active_record/relation/finder_methods.rb +64 -34
  161. data/lib/active_record/relation/merger.rb +12 -14
  162. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  163. data/lib/active_record/relation/predicate_builder/association_query_value.rb +19 -11
  164. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +8 -8
  165. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  166. data/lib/active_record/relation/predicate_builder.rb +25 -10
  167. data/lib/active_record/relation/query_attribute.rb +4 -2
  168. data/lib/active_record/relation/query_methods.rb +351 -124
  169. data/lib/active_record/relation/spawn_methods.rb +9 -25
  170. data/lib/active_record/relation/where_clause.rb +17 -30
  171. data/lib/active_record/relation.rb +590 -124
  172. data/lib/active_record/result.rb +131 -59
  173. data/lib/active_record/runtime_registry.rb +44 -21
  174. data/lib/active_record/sanitization.rb +33 -25
  175. data/lib/active_record/schema.rb +8 -6
  176. data/lib/active_record/schema_dumper.rb +66 -31
  177. data/lib/active_record/schema_migration.rb +31 -14
  178. data/lib/active_record/scoping/named.rb +6 -2
  179. data/lib/active_record/scoping.rb +0 -1
  180. data/lib/active_record/secure_token.rb +3 -3
  181. data/lib/active_record/signed_id.rb +68 -20
  182. data/lib/active_record/statement_cache.rb +29 -25
  183. data/lib/active_record/store.rb +51 -22
  184. data/lib/active_record/structured_event_subscriber.rb +85 -0
  185. data/lib/active_record/table_metadata.rb +7 -33
  186. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  187. data/lib/active_record/tasks/database_tasks.rb +118 -90
  188. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -42
  189. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
  190. data/lib/active_record/tasks/sqlite_database_tasks.rb +17 -28
  191. data/lib/active_record/test_databases.rb +14 -4
  192. data/lib/active_record/test_fixtures.rb +123 -89
  193. data/lib/active_record/testing/query_assertions.rb +127 -0
  194. data/lib/active_record/timestamp.rb +6 -4
  195. data/lib/active_record/token_for.rb +22 -12
  196. data/lib/active_record/touch_later.rb +1 -1
  197. data/lib/active_record/transaction.rb +129 -0
  198. data/lib/active_record/transactions.rb +97 -18
  199. data/lib/active_record/translation.rb +0 -2
  200. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  201. data/lib/active_record/type/internal/timezone.rb +7 -0
  202. data/lib/active_record/type/json.rb +15 -2
  203. data/lib/active_record/type/serialized.rb +12 -7
  204. data/lib/active_record/type/type_map.rb +1 -1
  205. data/lib/active_record/type_caster/connection.rb +5 -4
  206. data/lib/active_record/validations/associated.rb +10 -4
  207. data/lib/active_record/validations/uniqueness.rb +23 -18
  208. data/lib/active_record/validations.rb +4 -1
  209. data/lib/active_record.rb +203 -59
  210. data/lib/arel/alias_predication.rb +3 -1
  211. data/lib/arel/collectors/bind.rb +4 -2
  212. data/lib/arel/collectors/composite.rb +7 -0
  213. data/lib/arel/collectors/sql_string.rb +2 -2
  214. data/lib/arel/collectors/substitute_binds.rb +3 -3
  215. data/lib/arel/crud.rb +8 -11
  216. data/lib/arel/delete_manager.rb +5 -0
  217. data/lib/arel/nodes/binary.rb +1 -7
  218. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  219. data/lib/arel/nodes/count.rb +2 -2
  220. data/lib/arel/nodes/delete_statement.rb +4 -2
  221. data/lib/arel/nodes/function.rb +4 -10
  222. data/lib/arel/nodes/named_function.rb +2 -2
  223. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  224. data/lib/arel/nodes/node.rb +6 -5
  225. data/lib/arel/nodes/sql_literal.rb +8 -1
  226. data/lib/arel/nodes/update_statement.rb +4 -2
  227. data/lib/arel/nodes.rb +2 -4
  228. data/lib/arel/predications.rb +1 -1
  229. data/lib/arel/select_manager.rb +14 -5
  230. data/lib/arel/table.rb +3 -7
  231. data/lib/arel/tree_manager.rb +3 -2
  232. data/lib/arel/update_manager.rb +7 -1
  233. data/lib/arel/visitors/dot.rb +3 -3
  234. data/lib/arel/visitors/mysql.rb +9 -4
  235. data/lib/arel/visitors/postgresql.rb +56 -12
  236. data/lib/arel/visitors/sqlite.rb +80 -8
  237. data/lib/arel/visitors/to_sql.rb +34 -37
  238. data/lib/arel.rb +10 -4
  239. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  240. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  241. metadata +23 -14
  242. data/lib/active_record/explain_subscriber.rb +0 -34
  243. data/lib/active_record/normalization.rb +0 -167
  244. data/lib/active_record/relation/record_fetch_warning.rb +0 -49
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thread"
4
3
  require "concurrent/map"
5
4
  require "monitor"
6
5
 
@@ -9,30 +8,47 @@ require "active_record/connection_adapters/abstract/connection_pool/reaper"
9
8
 
10
9
  module ActiveRecord
11
10
  module ConnectionAdapters
12
- module AbstractPool # :nodoc:
13
- end
14
-
15
11
  class NullPool # :nodoc:
16
- include ConnectionAdapters::AbstractPool
17
-
18
- class NullConfig # :nodoc:
19
- def method_missing(*)
12
+ class NullConfig
13
+ def method_missing(...)
20
14
  nil
21
15
  end
22
16
  end
23
- NULL_CONFIG = NullConfig.new # :nodoc:
17
+ NULL_CONFIG = NullConfig.new
18
+
19
+ def initialize
20
+ super()
21
+ @mutex = Mutex.new
22
+ @server_version = nil
23
+ end
24
+
25
+ def server_version(connection) # :nodoc:
26
+ @server_version || @mutex.synchronize { @server_version ||= connection.get_database_version }
27
+ end
24
28
 
25
29
  def schema_reflection
26
30
  SchemaReflection.new(nil)
27
31
  end
28
32
 
29
- def connection_class; end
33
+ def schema_cache; end
34
+ def query_cache; end
35
+ def connection_descriptor; end
30
36
  def checkin(_); end
31
37
  def remove(_); end
32
38
  def async_executor; end
39
+
33
40
  def db_config
34
41
  NULL_CONFIG
35
42
  end
43
+
44
+ def dirties_query_cache
45
+ true
46
+ end
47
+
48
+ def pool_transaction_isolation_level; end
49
+ def pool_transaction_isolation_level=(isolation_level)
50
+ raise NotImplementedError, "This method should never be called"
51
+ end
36
52
  end
37
53
 
38
54
  # = Active Record Connection Pool
@@ -58,7 +74,7 @@ module ActiveRecord
58
74
  # Connections can be obtained and used from a connection pool in several
59
75
  # ways:
60
76
  #
61
- # 1. Simply use {ActiveRecord::Base.connection}[rdoc-ref:ConnectionHandling.connection].
77
+ # 1. Simply use {ActiveRecord::Base.lease_connection}[rdoc-ref:ConnectionHandling#lease_connection].
62
78
  # When you're done with the connection(s) and wish it to be returned to the pool, you call
63
79
  # {ActiveRecord::Base.connection_handler.clear_active_connections!}[rdoc-ref:ConnectionAdapters::ConnectionHandler#clear_active_connections!].
64
80
  # This is the default behavior for Active Record when used in conjunction with
@@ -85,13 +101,21 @@ module ActiveRecord
85
101
  # There are several connection-pooling-related options that you can add to
86
102
  # your database connection configuration:
87
103
  #
88
- # * +pool+: maximum number of connections the pool may manage (default 5).
89
- # * +idle_timeout+: number of seconds that a connection will be kept
90
- # unused in the pool before it is automatically disconnected (default
91
- # 300 seconds). Set this to zero to keep connections forever.
92
104
  # * +checkout_timeout+: number of seconds to wait for a connection to
93
105
  # become available before giving up and raising a timeout error (default
94
106
  # 5 seconds).
107
+ # * +idle_timeout+: number of seconds that a connection will be kept
108
+ # unused in the pool before it is automatically disconnected (default
109
+ # 300 seconds). Set this to zero to keep connections forever.
110
+ # * +keepalive+: number of seconds between keepalive checks if the
111
+ # connection has been idle (default 600 seconds).
112
+ # * +max_age+: number of seconds the pool will allow the connection to
113
+ # exist before retiring it at next checkin. (default Float::INFINITY).
114
+ # * +max_connections+: maximum number of connections the pool may manage (default 5).
115
+ # Set to +nil+ or -1 for unlimited connections.
116
+ # * +min_connections+: minimum number of connections the pool will open and maintain (default 0).
117
+ # * +pool_jitter+: maximum reduction factor to apply to +max_age+ and
118
+ # +keepalive+ intervals (default 0.2; range 0.0-1.0).
95
119
  #
96
120
  #--
97
121
  # Synchronization policy:
@@ -99,17 +123,124 @@ module ActiveRecord
99
123
  # * access to these instance variables needs to be in +synchronize+:
100
124
  # * @connections
101
125
  # * @now_connecting
126
+ # * @maintaining
102
127
  # * private methods that require being called in a +synchronize+ blocks
103
128
  # are now explicitly documented
104
129
  class ConnectionPool
130
+ # Prior to 3.3.5, WeakKeyMap had a use after free bug
131
+ # https://bugs.ruby-lang.org/issues/20688
132
+ if ObjectSpace.const_defined?(:WeakKeyMap) && RUBY_VERSION >= "3.3.5"
133
+ WeakThreadKeyMap = ObjectSpace::WeakKeyMap
134
+ else
135
+ class WeakThreadKeyMap # :nodoc:
136
+ # FIXME: On 3.3 we could use ObjectSpace::WeakKeyMap
137
+ # but it currently causes GC crashes: https://github.com/byroot/rails/pull/3
138
+ def initialize
139
+ @map = {}
140
+ end
141
+
142
+ def clear
143
+ @map.clear
144
+ end
145
+
146
+ def [](key)
147
+ @map[key]
148
+ end
149
+
150
+ def []=(key, value)
151
+ @map.select! { |c, _| c&.alive? }
152
+ @map[key] = value
153
+ end
154
+ end
155
+ end
156
+
157
+ class Lease # :nodoc:
158
+ attr_accessor :connection, :sticky
159
+
160
+ def initialize
161
+ @connection = nil
162
+ @sticky = nil
163
+ end
164
+
165
+ def release
166
+ conn = @connection
167
+ @connection = nil
168
+ @sticky = nil
169
+ conn
170
+ end
171
+
172
+ def clear(connection)
173
+ if @connection == connection
174
+ @connection = nil
175
+ @sticky = nil
176
+ true
177
+ else
178
+ false
179
+ end
180
+ end
181
+ end
182
+
183
+ if RUBY_ENGINE == "ruby"
184
+ # Thanks to the GVL, the LeaseRegistry doesn't need to be synchronized on MRI
185
+ class LeaseRegistry < WeakThreadKeyMap # :nodoc:
186
+ def [](context)
187
+ super || (self[context] = Lease.new)
188
+ end
189
+ end
190
+ else
191
+ class LeaseRegistry # :nodoc:
192
+ def initialize
193
+ @mutex = Mutex.new
194
+ @map = WeakThreadKeyMap.new
195
+ end
196
+
197
+ def [](context)
198
+ @mutex.synchronize do
199
+ @map[context] ||= Lease.new
200
+ end
201
+ end
202
+
203
+ def clear
204
+ @mutex.synchronize do
205
+ @map.clear
206
+ end
207
+ end
208
+ end
209
+ end
210
+
211
+ module ExecutorHooks # :nodoc:
212
+ class << self
213
+ def run
214
+ # noop
215
+ end
216
+
217
+ def complete(_)
218
+ ActiveRecord::Base.connection_handler.each_connection_pool do |pool|
219
+ if (connection = pool.active_connection?)
220
+ transaction = connection.current_transaction
221
+ if transaction.closed? || !transaction.joinable?
222
+ pool.release_connection
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ class << self
231
+ def install_executor_hooks(executor = ActiveSupport::Executor)
232
+ executor.register_hook(ExecutorHooks)
233
+ end
234
+ end
235
+
105
236
  include MonitorMixin
106
- include QueryCache::ConnectionPoolConfiguration
107
- include ConnectionAdapters::AbstractPool
237
+ prepend QueryCache::ConnectionPoolConfiguration
108
238
 
109
239
  attr_accessor :automatic_reconnect, :checkout_timeout
110
- attr_reader :db_config, :size, :reaper, :pool_config, :async_executor, :role, :shard
240
+ attr_reader :db_config, :max_connections, :min_connections, :max_age, :keepalive, :reaper, :pool_config, :async_executor, :role, :shard
241
+ alias :size :max_connections
111
242
 
112
- delegate :schema_reflection, :schema_reflection=, to: :pool_config
243
+ delegate :schema_reflection, :server_version, to: :pool_config
113
244
 
114
245
  # Creates a new ConnectionPool object. +pool_config+ is a PoolConfig
115
246
  # object which describes database connection information (e.g. adapter,
@@ -127,7 +258,10 @@ module ActiveRecord
127
258
 
128
259
  @checkout_timeout = db_config.checkout_timeout
129
260
  @idle_timeout = db_config.idle_timeout
130
- @size = db_config.pool
261
+ @max_connections = db_config.max_connections
262
+ @min_connections = db_config.min_connections
263
+ @max_age = db_config.max_age
264
+ @keepalive = db_config.keepalive
131
265
 
132
266
  # This variable tracks the cache of threads mapped to reserved connections, with the
133
267
  # sole purpose of speeding up the +connection+ method. It is not the authoritative
@@ -137,9 +271,9 @@ module ActiveRecord
137
271
  # then that +thread+ does indeed own that +conn+. However, an absence of such
138
272
  # mapping does not mean that the +thread+ doesn't own the said connection. In
139
273
  # that case +conn.owner+ attr should be consulted.
140
- # Access and modification of <tt>@thread_cached_conns</tt> does not require
274
+ # Access and modification of <tt>@leases</tt> does not require
141
275
  # synchronization.
142
- @thread_cached_conns = Concurrent::Map.new(initial_capacity: @size)
276
+ @leases = LeaseRegistry.new
143
277
 
144
278
  @connections = []
145
279
  @automatic_reconnect = true
@@ -149,89 +283,212 @@ module ActiveRecord
149
283
  # currently in the process of independently establishing connections to the DB.
150
284
  @now_connecting = 0
151
285
 
286
+ # Sometimes otherwise-idle connections are temporarily held by the Reaper for
287
+ # maintenance. This variable tracks the number of connections currently in that
288
+ # state -- if a thread requests a connection and there are none available, it
289
+ # will await any in-maintenance connections in preference to creating a new one.
290
+ @maintaining = 0
291
+
152
292
  @threads_blocking_new_connections = 0
153
293
 
154
294
  @available = ConnectionLeasingQueue.new self
155
-
156
- @lock_thread = false
295
+ @pinned_connection = nil
296
+ @pinned_connections_depth = 0
157
297
 
158
298
  @async_executor = build_async_executor
159
299
 
300
+ @schema_cache = nil
301
+
302
+ @activated = false
303
+ @original_context = ActiveSupport::IsolatedExecutionState.context
304
+
305
+ @reaper_lock = Monitor.new
160
306
  @reaper = Reaper.new(self, db_config.reaping_frequency)
161
307
  @reaper.run
162
308
  end
163
309
 
164
- def lock_thread=(lock_thread)
165
- if lock_thread
166
- @lock_thread = ActiveSupport::IsolatedExecutionState.context
167
- else
168
- @lock_thread = nil
169
- end
310
+ def inspect # :nodoc:
311
+ name_field = " name=#{name_inspect}" if name_inspect
312
+ shard_field = " shard=#{shard_inspect}" if shard_inspect
170
313
 
171
- if (active_connection = @thread_cached_conns[connection_cache_key(current_thread)])
172
- active_connection.lock_thread = @lock_thread
173
- end
314
+ "#<#{self.class.name} env_name=#{db_config.env_name.inspect}#{name_field} role=#{role.inspect}#{shard_field}>"
315
+ end
316
+
317
+ def schema_cache
318
+ @schema_cache ||= BoundSchemaReflection.new(schema_reflection, self)
319
+ end
320
+
321
+ def schema_reflection=(schema_reflection)
322
+ pool_config.schema_reflection = schema_reflection
323
+ @schema_cache = nil
324
+ end
325
+
326
+ def migration_context # :nodoc:
327
+ MigrationContext.new(migrations_paths, schema_migration, internal_metadata)
328
+ end
329
+
330
+ def migrations_paths # :nodoc:
331
+ db_config.migrations_paths || Migrator.migrations_paths
332
+ end
333
+
334
+ def schema_migration # :nodoc:
335
+ SchemaMigration.new(self)
336
+ end
337
+
338
+ def internal_metadata # :nodoc:
339
+ InternalMetadata.new(self)
340
+ end
341
+
342
+ def activate
343
+ @activated = true
344
+ end
345
+
346
+ def activated?
347
+ @activated
174
348
  end
175
349
 
176
350
  # Retrieve the connection associated with the current thread, or call
177
351
  # #checkout to obtain one if necessary.
178
352
  #
179
- # #connection can be called any number of times; the connection is
353
+ # #lease_connection can be called any number of times; the connection is
180
354
  # held in a cache keyed by a thread.
181
- def connection
182
- @thread_cached_conns[connection_cache_key(current_thread)] ||= checkout
355
+ def lease_connection
356
+ lease = connection_lease
357
+ lease.connection ||= checkout
358
+ lease.sticky = true
359
+ lease.connection
183
360
  end
184
361
 
185
- def connection_class # :nodoc:
186
- pool_config.connection_class
362
+ def permanent_lease? # :nodoc:
363
+ connection_lease.sticky.nil?
364
+ end
365
+
366
+ def pin_connection!(lock_thread) # :nodoc:
367
+ @pinned_connection ||= (connection_lease&.connection || checkout)
368
+ @pinned_connections_depth += 1
369
+
370
+ # Any leased connection must be in @connections otherwise
371
+ # some methods like #connected? won't behave correctly
372
+ unless @connections.include?(@pinned_connection)
373
+ @connections << @pinned_connection
374
+ end
375
+
376
+ @pinned_connection.lock_thread = ActiveSupport::IsolatedExecutionState.context if lock_thread
377
+ @pinned_connection.pinned = true
378
+ @pinned_connection.verify! # eagerly validate the connection
379
+ @pinned_connection.begin_transaction joinable: false, _lazy: false
380
+ end
381
+
382
+ def unpin_connection! # :nodoc:
383
+ raise "There isn't a pinned connection #{object_id}" unless @pinned_connection
384
+
385
+ clean = true
386
+ @pinned_connection.lock.synchronize do
387
+ @pinned_connections_depth -= 1
388
+ connection = @pinned_connection
389
+ @pinned_connection = nil if @pinned_connections_depth.zero?
390
+
391
+ if connection.transaction_open?
392
+ connection.rollback_transaction
393
+ else
394
+ # Something committed or rolled back the transaction
395
+ clean = false
396
+ connection.reset!
397
+ end
398
+
399
+ if @pinned_connection.nil?
400
+ connection.pinned = false
401
+ connection.steal!
402
+ connection.lock_thread = nil
403
+ checkin(connection)
404
+ end
405
+ end
406
+
407
+ clean
408
+ end
409
+
410
+ def connection_descriptor # :nodoc:
411
+ pool_config.connection_descriptor
187
412
  end
188
- alias :connection_klass :connection_class
189
- deprecate :connection_klass, deprecator: ActiveRecord.deprecator
190
413
 
191
414
  # Returns true if there is an open connection being used for the current thread.
192
415
  #
193
416
  # This method only works for connections that have been obtained through
194
- # #connection or #with_connection methods. Connections obtained through
417
+ # #lease_connection or #with_connection methods. Connections obtained through
195
418
  # #checkout will not be detected by #active_connection?
196
419
  def active_connection?
197
- @thread_cached_conns[connection_cache_key(current_thread)]
420
+ connection_lease.connection
198
421
  end
422
+ alias_method :active_connection, :active_connection? # :nodoc:
199
423
 
200
424
  # Signal that the thread is finished with the current connection.
201
425
  # #release_connection releases the connection-thread association
202
426
  # and returns the connection to the pool.
203
427
  #
204
428
  # This method only works for connections that have been obtained through
205
- # #connection or #with_connection methods, connections obtained through
429
+ # #lease_connection or #with_connection methods, connections obtained through
206
430
  # #checkout will not be automatically released.
207
- def release_connection(owner_thread = ActiveSupport::IsolatedExecutionState.context)
208
- if conn = @thread_cached_conns.delete(connection_cache_key(owner_thread))
431
+ def release_connection(existing_lease = nil)
432
+ return if self.discarded?
433
+
434
+ if conn = connection_lease.release
209
435
  checkin conn
436
+ return true
210
437
  end
438
+ false
211
439
  end
212
440
 
213
441
  # Yields a connection from the connection pool to the block. If no connection
214
442
  # is already checked out by the current thread, a connection will be checked
215
443
  # out from the pool, yielded to the block, and then returned to the pool when
216
444
  # the block is finished. If a connection has already been checked out on the
217
- # current thread, such as via #connection or #with_connection, that existing
445
+ # current thread, such as via #lease_connection or #with_connection, that existing
218
446
  # connection will be the one yielded and it will not be returned to the pool
219
447
  # automatically at the end of the block; it is expected that such an existing
220
448
  # connection will be properly returned to the pool by the code that checked
221
449
  # it out.
222
- def with_connection
223
- unless conn = @thread_cached_conns[connection_cache_key(ActiveSupport::IsolatedExecutionState.context)]
224
- conn = connection
225
- fresh_connection = true
450
+ def with_connection(prevent_permanent_checkout: false)
451
+ lease = connection_lease
452
+ sticky_was = lease.sticky
453
+ lease.sticky = false if prevent_permanent_checkout
454
+
455
+ if lease.connection
456
+ begin
457
+ yield lease.connection
458
+ ensure
459
+ lease.sticky = sticky_was if prevent_permanent_checkout && !sticky_was
460
+ end
461
+ else
462
+ begin
463
+ yield lease.connection = checkout
464
+ ensure
465
+ lease.sticky = sticky_was if prevent_permanent_checkout && !sticky_was
466
+ release_connection(lease) unless lease.sticky
467
+ end
468
+ end
469
+ end
470
+
471
+ def with_pool_transaction_isolation_level(isolation_level, transaction_open, &block) # :nodoc:
472
+ if !ActiveRecord.default_transaction_isolation_level.nil?
473
+ begin
474
+ if transaction_open && self.pool_transaction_isolation_level != ActiveRecord.default_transaction_isolation_level
475
+ raise ActiveRecord::TransactionIsolationError, "cannot set default isolation level while transaction is open"
476
+ end
477
+
478
+ old_level = self.pool_transaction_isolation_level
479
+ self.pool_transaction_isolation_level = isolation_level
480
+ yield
481
+ ensure
482
+ self.pool_transaction_isolation_level = old_level
483
+ end
484
+ else
485
+ yield
226
486
  end
227
- yield conn
228
- ensure
229
- release_connection if fresh_connection
230
487
  end
231
488
 
232
489
  # Returns true if a connection has already been opened.
233
490
  def connected?
234
- synchronize { @connections.any? }
491
+ synchronize { @connections.any?(&:connected?) }
235
492
  end
236
493
 
237
494
  # Returns an array containing the connections currently in the pool.
@@ -256,17 +513,26 @@ module ActiveRecord
256
513
  # connections in the pool within a timeout interval (default duration is
257
514
  # <tt>spec.db_config.checkout_timeout * 2</tt> seconds).
258
515
  def disconnect(raise_on_acquisition_timeout = true)
259
- with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do
260
- synchronize do
261
- @connections.each do |conn|
262
- if conn.in_use?
263
- conn.steal!
264
- checkin conn
516
+ @reaper_lock.synchronize do
517
+ return if self.discarded?
518
+
519
+ with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do
520
+ synchronize do
521
+ return if self.discarded?
522
+ @connections.each do |conn|
523
+ if conn.in_use?
524
+ conn.steal!
525
+ checkin conn
526
+ end
527
+ conn.disconnect!
265
528
  end
266
- conn.disconnect!
529
+ @connections = []
530
+ @leases.clear
531
+ @available.clear
532
+
533
+ # Stop maintaining the minimum size until reactivated
534
+ @activated = false
267
535
  end
268
- @connections = []
269
- @available.clear
270
536
  end
271
537
  end
272
538
  end
@@ -287,12 +553,14 @@ module ActiveRecord
287
553
  #
288
554
  # See AbstractAdapter#discard!
289
555
  def discard! # :nodoc:
290
- synchronize do
291
- return if self.discarded?
292
- @connections.each do |conn|
293
- conn.discard!
556
+ @reaper_lock.synchronize do
557
+ synchronize do
558
+ return if self.discarded?
559
+ @connections.each do |conn|
560
+ conn.discard!
561
+ end
562
+ @connections = @available = @leases = nil
294
563
  end
295
- @connections = @available = @thread_cached_conns = nil
296
564
  end
297
565
  end
298
566
 
@@ -300,7 +568,17 @@ module ActiveRecord
300
568
  @connections.nil?
301
569
  end
302
570
 
303
- # Clears the cache which maps classes and re-connects connections that
571
+ def maintainable? # :nodoc:
572
+ synchronize do
573
+ @connections&.size&.> 0 || (activated? && @min_connections > 0)
574
+ end
575
+ end
576
+
577
+ def reaper_lock(&block) # :nodoc:
578
+ @reaper_lock.synchronize(&block)
579
+ end
580
+
581
+ # Clears reloadable connections from the pool and re-connects connections that
304
582
  # require reloading.
305
583
  #
306
584
  # Raises:
@@ -323,7 +601,7 @@ module ActiveRecord
323
601
  end
324
602
  end
325
603
 
326
- # Clears the cache which maps classes and re-connects connections that
604
+ # Clears reloadable connections from the pool and re-connects connections that
327
605
  # require reloading.
328
606
  #
329
607
  # The pool first tries to gain ownership of all connections. If unable to
@@ -350,9 +628,26 @@ module ActiveRecord
350
628
  # Raises:
351
629
  # - ActiveRecord::ConnectionTimeoutError no connection can be obtained from the pool.
352
630
  def checkout(checkout_timeout = @checkout_timeout)
353
- connection = checkout_and_verify(acquire_connection(checkout_timeout))
354
- connection.lock_thread = @lock_thread
355
- connection
631
+ return checkout_and_verify(acquire_connection(checkout_timeout)) unless @pinned_connection
632
+
633
+ @pinned_connection.lock.synchronize do
634
+ synchronize do
635
+ # The pinned connection may have been cleaned up before we synchronized, so check if it is still present
636
+ if @pinned_connection
637
+ @pinned_connection.verify!
638
+
639
+ # Any leased connection must be in @connections otherwise
640
+ # some methods like #connected? won't behave correctly
641
+ unless @connections.include?(@pinned_connection)
642
+ @connections << @pinned_connection
643
+ end
644
+
645
+ @pinned_connection
646
+ else
647
+ checkout_and_verify(acquire_connection(checkout_timeout))
648
+ end
649
+ end
650
+ end
356
651
  end
357
652
 
358
653
  # Check-in a database connection back into the pool, indicating that you
@@ -361,15 +656,12 @@ module ActiveRecord
361
656
  # +conn+: an AbstractAdapter object, which was obtained by earlier by
362
657
  # calling #checkout on this pool.
363
658
  def checkin(conn)
659
+ return if @pinned_connection.equal?(conn)
660
+
364
661
  conn.lock.synchronize do
365
662
  synchronize do
366
- remove_connection_from_thread_cache conn
367
-
368
- conn._run_checkin_callbacks do
369
- conn.expire
370
- end
371
-
372
- conn.lock_thread = nil
663
+ connection_lease.clear(conn)
664
+ conn.expire
373
665
  @available.add conn
374
666
  end
375
667
  end
@@ -387,7 +679,7 @@ module ActiveRecord
387
679
  @available.delete conn
388
680
 
389
681
  # @available.any_waiting? => true means that prior to removing this
390
- # conn, the pool was at its max size (@connections.size == @size).
682
+ # conn, the pool was at its max size (@connections.size == @max_connections).
391
683
  # This would mean that any threads stuck waiting in the queue wouldn't
392
684
  # know they could checkout_new_connection, so let's do it for them.
393
685
  # Because condition-wait loop is encapsulated in the Queue class
@@ -395,14 +687,14 @@ module ActiveRecord
395
687
  # that are "stuck" there are helpless. They have no way of creating
396
688
  # new connections and are completely reliant on us feeding available
397
689
  # connections into the Queue.
398
- needs_new_connection = @available.any_waiting?
690
+ needs_new_connection = @available.num_waiting > @maintaining
399
691
  end
400
692
 
401
693
  # This is intentionally done outside of the synchronized section as we
402
694
  # would like not to hold the main mutex while checking out new connections.
403
695
  # Thus there is some chance that needs_new_connection information is now
404
696
  # stale, we can live with that (bulk_make_new_connections will make
405
- # sure not to exceed the pool's @size limit).
697
+ # sure not to exceed the pool's @max_connections limit).
406
698
  bulk_make_new_connections(1) if needs_new_connection
407
699
  end
408
700
 
@@ -435,11 +727,27 @@ module ActiveRecord
435
727
  def flush(minimum_idle = @idle_timeout)
436
728
  return if minimum_idle.nil?
437
729
 
438
- idle_connections = synchronize do
730
+ removed_connections = synchronize do
439
731
  return if self.discarded?
440
- @connections.select do |conn|
732
+
733
+ idle_connections = @connections.select do |conn|
441
734
  !conn.in_use? && conn.seconds_idle >= minimum_idle
442
- end.each do |conn|
735
+ end.sort_by { |conn| -conn.seconds_idle } # sort longest idle first
736
+
737
+ # Don't go below our configured pool minimum unless we're flushing
738
+ # everything
739
+ idles_to_retain =
740
+ if minimum_idle > 0
741
+ @min_connections - (@connections.size - idle_connections.size)
742
+ else
743
+ 0
744
+ end
745
+
746
+ if idles_to_retain > 0
747
+ idle_connections.pop idles_to_retain
748
+ end
749
+
750
+ idle_connections.each do |conn|
443
751
  conn.lease
444
752
 
445
753
  @available.delete conn
@@ -447,22 +755,109 @@ module ActiveRecord
447
755
  end
448
756
  end
449
757
 
450
- idle_connections.each do |conn|
758
+ removed_connections.each do |conn|
451
759
  conn.disconnect!
452
760
  end
453
761
  end
454
762
 
455
763
  # Disconnect all currently idle connections. Connections currently checked
456
- # out are unaffected.
764
+ # out are unaffected. The pool will stop maintaining its minimum size until
765
+ # it is reactivated (such as by a subsequent checkout).
457
766
  def flush!
458
767
  reap
459
768
  flush(-1)
769
+
770
+ # Stop maintaining the minimum size until reactivated
771
+ @activated = false
772
+ end
773
+
774
+ # Ensure that the pool contains at least the configured minimum number of
775
+ # connections.
776
+ def prepopulate
777
+ need_new_connections = nil
778
+
779
+ synchronize do
780
+ return if self.discarded?
781
+
782
+ # We don't want to start prepopulating until we know the pool is wanted,
783
+ # so we can avoid maintaining full pools in one-off scripts etc.
784
+ return unless @activated
785
+
786
+ need_new_connections = @connections.size < @min_connections
787
+ end
788
+
789
+ if need_new_connections
790
+ while new_conn = try_to_checkout_new_connection { @connections.size < @min_connections }
791
+ new_conn.allow_preconnect = true
792
+ checkin(new_conn)
793
+ end
794
+ end
795
+ end
796
+
797
+ def retire_old_connections(max_age = @max_age)
798
+ max_age ||= Float::INFINITY
799
+
800
+ sequential_maintenance -> c { c.connection_age&.>= c.pool_jitter(max_age) } do |conn|
801
+ # Disconnect, then return the adapter to the pool. Preconnect will
802
+ # handle the rest.
803
+ conn.disconnect!
804
+ end
805
+ end
806
+
807
+ # Preconnect all connections in the pool. This saves pool users from
808
+ # having to wait for a connection to be established when first using it
809
+ # after checkout.
810
+ def preconnect
811
+ sequential_maintenance -> c { (!c.connected? || !c.verified?) && c.allow_preconnect } do |conn|
812
+ conn.connect!
813
+ rescue
814
+ # Wholesale rescue: there's nothing we can do but move on. The
815
+ # connection will go back to the pool, and the next consumer will
816
+ # presumably try to connect again -- which will either work, or
817
+ # fail and they'll be able to report the exception.
818
+ end
819
+ end
820
+
821
+ # Prod any connections that have been idle for longer than the configured
822
+ # keepalive time. This will incidentally verify the connection is still
823
+ # alive, but the main purpose is to show the server (and any intermediate
824
+ # network hops) that we're still here and using the connection.
825
+ def keep_alive(threshold = @keepalive)
826
+ return if threshold.nil?
827
+
828
+ sequential_maintenance -> c { (c.seconds_since_last_activity || 0) > c.pool_jitter(threshold) } do |conn|
829
+ # conn.active? will cause some amount of network activity, which is all
830
+ # we need to provide a keepalive signal.
831
+ #
832
+ # If it returns false, the connection is already broken; disconnect,
833
+ # so it can be found and repaired.
834
+ conn.disconnect! unless conn.active?
835
+ end
836
+ end
837
+
838
+ # Immediately mark all current connections as due for replacement,
839
+ # equivalent to them having reached +max_age+ -- even if there is
840
+ # no +max_age+ configured.
841
+ def recycle!
842
+ synchronize do
843
+ return if self.discarded?
844
+
845
+ @connections.each do |conn|
846
+ conn.force_retirement
847
+ end
848
+ end
849
+
850
+ retire_old_connections
460
851
  end
461
852
 
462
853
  def num_waiting_in_queue # :nodoc:
463
854
  @available.num_waiting
464
855
  end
465
856
 
857
+ def num_available_in_queue # :nodoc:
858
+ @available.size
859
+ end
860
+
466
861
  # Returns the connection pool's usage statistic.
467
862
  #
468
863
  # ActiveRecord::Base.connection_pool.stat # => { size: 15, connections: 1, busy: 1, dead: 0, idle: 0, waiting: 0, checkout_timeout: 5 }
@@ -485,12 +880,36 @@ module ActiveRecord
485
880
  Thread.pass
486
881
  end
487
882
 
883
+ def new_connection # :nodoc:
884
+ connection = db_config.new_connection
885
+ connection.pool = self
886
+ connection
887
+ rescue ConnectionNotEstablished => ex
888
+ raise ex.set_pool(self)
889
+ end
890
+
891
+ def pool_transaction_isolation_level
892
+ isolation_level_key = "activerecord_pool_transaction_isolation_level_#{db_config.name}"
893
+ ActiveSupport::IsolatedExecutionState[isolation_level_key]
894
+ end
895
+
896
+ def pool_transaction_isolation_level=(isolation_level)
897
+ isolation_level_key = "activerecord_pool_transaction_isolation_level_#{db_config.name}"
898
+ ActiveSupport::IsolatedExecutionState[isolation_level_key] = isolation_level
899
+ end
900
+
488
901
  private
902
+ def connection_lease
903
+ @leases[ActiveSupport::IsolatedExecutionState.context]
904
+ end
905
+
489
906
  def build_async_executor
490
907
  case ActiveRecord.async_query_executor
491
908
  when :multi_thread_pool
492
909
  if @db_config.max_threads > 0
910
+ name_with_shard = [name_inspect, shard_inspect].join("-").tr("_", "-")
493
911
  Concurrent::ThreadPoolExecutor.new(
912
+ name: "ActiveRecord-#{name_with_shard}-async-query-executor",
494
913
  min_threads: @db_config.min_threads,
495
914
  max_threads: @db_config.max_threads,
496
915
  max_queue: @db_config.max_queue,
@@ -502,11 +921,109 @@ module ActiveRecord
502
921
  end
503
922
  end
504
923
 
924
+ # Perform maintenance work on pool connections. This method will
925
+ # select a connection to work on by calling the +candidate_selector+
926
+ # proc while holding the pool lock. If a connection is selected, it
927
+ # will be checked out for maintenance and passed to the
928
+ # +maintenance_work+ proc. The connection will always be returned to
929
+ # the pool after the proc completes.
930
+ #
931
+ # If the pool has async threads, all work will be scheduled there.
932
+ # Otherwise, this method will block until all work is complete.
933
+ #
934
+ # Each connection will only be processed once per call to this method,
935
+ # but (particularly in the async case) there is no protection against
936
+ # a second call to this method starting to work through the list
937
+ # before the first call has completed. (Though regular pool behavior
938
+ # will prevent two instances from working on the same specific
939
+ # connection at the same time.)
940
+ def sequential_maintenance(candidate_selector, &maintenance_work)
941
+ # This hash doesn't need to be synchronized, because it's only
942
+ # used by one thread at a time: the +perform_work+ block gives
943
+ # up its right to +connections_visited+ when it schedules the
944
+ # next iteration.
945
+ connections_visited = Hash.new(false)
946
+ connections_visited.compare_by_identity
947
+
948
+ perform_work = lambda do
949
+ connection_to_maintain = nil
950
+
951
+ synchronize do
952
+ unless self.discarded?
953
+ if connection_to_maintain = @connections.select { |conn| !conn.in_use? }.select(&candidate_selector).sort_by(&:seconds_idle).find { |conn| !connections_visited[conn] }
954
+ checkout_for_maintenance connection_to_maintain
955
+ end
956
+ end
957
+ end
958
+
959
+ if connection_to_maintain
960
+ connections_visited[connection_to_maintain] = true
961
+
962
+ # If we're running async, we can schedule the next round of work
963
+ # as soon as we've grabbed a connection to work on.
964
+ @async_executor&.post(&perform_work)
965
+
966
+ begin
967
+ maintenance_work.call connection_to_maintain
968
+ ensure
969
+ return_from_maintenance connection_to_maintain
970
+ end
971
+
972
+ true
973
+ end
974
+ end
975
+
976
+ if @async_executor
977
+ @async_executor.post(&perform_work)
978
+ else
979
+ nil while perform_work.call
980
+ end
981
+ end
982
+
983
+ # Directly check a specific connection out of the pool. Skips callbacks.
984
+ #
985
+ # The connection must later either #return_from_maintenance or
986
+ # #remove_from_maintenance, or the pool will hang.
987
+ def checkout_for_maintenance(conn)
988
+ synchronize do
989
+ @maintaining += 1
990
+ @available.delete(conn)
991
+ conn.lease
992
+ conn
993
+ end
994
+ end
995
+
996
+ # Return a connection to the pool after it has been checked out for
997
+ # maintenance. Does not update the connection's idle time, and skips
998
+ # callbacks.
999
+ #--
1000
+ # We assume that a connection that has required maintenance is less
1001
+ # desirable (either it's been idle for a long time, or it was just
1002
+ # created and hasn't been used yet). We'll put it at the back of the
1003
+ # queue.
1004
+ def return_from_maintenance(conn)
1005
+ synchronize do
1006
+ conn.expire(false)
1007
+ @available.add_back(conn)
1008
+ @maintaining -= 1
1009
+ end
1010
+ end
1011
+
1012
+ # Remove a connection from the pool after it has been checked out for
1013
+ # maintenance. It will be automatically replaced with a new connection if
1014
+ # necessary.
1015
+ def remove_from_maintenance(conn)
1016
+ synchronize do
1017
+ @maintaining -= 1
1018
+ remove conn
1019
+ end
1020
+ end
1021
+
505
1022
  #--
506
1023
  # this is unfortunately not concurrent
507
1024
  def bulk_make_new_connections(num_new_conns_needed)
508
1025
  num_new_conns_needed.times do
509
- # try_to_checkout_new_connection will not exceed pool's @size limit
1026
+ # try_to_checkout_new_connection will not exceed pool's @max_connections limit
510
1027
  if new_conn = try_to_checkout_new_connection
511
1028
  # make the new_conn available to the starving threads stuck @available Queue
512
1029
  checkin(new_conn)
@@ -514,32 +1031,23 @@ module ActiveRecord
514
1031
  end
515
1032
  end
516
1033
 
517
- #--
518
- # From the discussion on GitHub:
519
- # https://github.com/rails/rails/pull/14938#commitcomment-6601951
520
- # This hook-in method allows for easier monkey-patching fixes needed by
521
- # JRuby users that use Fibers.
522
- def connection_cache_key(thread)
523
- thread
524
- end
525
-
526
- def current_thread
527
- @lock_thread || ActiveSupport::IsolatedExecutionState.context
528
- end
529
-
530
1034
  # Take control of all existing connections so a "group" action such as
531
1035
  # reload/disconnect can be performed safely. It is no longer enough to
532
1036
  # wrap it in +synchronize+ because some pool's actions are allowed
533
1037
  # to be performed outside of the main +synchronize+ block.
534
1038
  def with_exclusively_acquired_all_connections(raise_on_acquisition_timeout = true)
535
- with_new_connections_blocked do
536
- attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout)
537
- yield
1039
+ @reaper_lock.synchronize do
1040
+ with_new_connections_blocked do
1041
+ attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout)
1042
+ yield
1043
+ end
538
1044
  end
539
1045
  end
540
1046
 
541
1047
  def attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout = true)
542
1048
  collected_conns = synchronize do
1049
+ reap # No need to wait for dead owners
1050
+
543
1051
  # account for our own connections
544
1052
  @connections.select { |conn| conn.owner == ActiveSupport::IsolatedExecutionState.context }
545
1053
  end
@@ -551,6 +1059,7 @@ module ActiveRecord
551
1059
  loop do
552
1060
  synchronize do
553
1061
  return if collected_conns.size == @connections.size && @now_connecting == 0
1062
+
554
1063
  remaining_timeout = timeout_time - Process.clock_gettime(Process::CLOCK_MONOTONIC)
555
1064
  remaining_timeout = 0 if remaining_timeout < 0
556
1065
  conn = checkout_for_exclusive_access(remaining_timeout)
@@ -601,7 +1110,7 @@ module ActiveRecord
601
1110
 
602
1111
  msg << " (#{thread_report.join(', ')})" if thread_report.any?
603
1112
 
604
- raise ExclusiveConnectionTimeoutError, msg
1113
+ raise ExclusiveConnectionTimeoutError.new(msg, connection_pool: self)
605
1114
  end
606
1115
 
607
1116
  def with_new_connections_blocked
@@ -651,54 +1160,76 @@ module ActiveRecord
651
1160
  # <tt>synchronize { conn.lease }</tt> in this method, but by leaving it to <tt>@available.poll</tt>
652
1161
  # and +try_to_checkout_new_connection+ we can piggyback on +synchronize+ sections
653
1162
  # of the said methods and avoid an additional +synchronize+ overhead.
654
- if conn = @available.poll || try_to_checkout_new_connection
1163
+ if conn = @available.poll || try_to_queue_for_background_connection(checkout_timeout) || try_to_checkout_new_connection
655
1164
  conn
656
1165
  else
657
1166
  reap
658
1167
  # Retry after reaping, which may return an available connection,
659
1168
  # remove an inactive connection, or both
660
- if conn = @available.poll || try_to_checkout_new_connection
1169
+ if conn = @available.poll || try_to_queue_for_background_connection(checkout_timeout) || try_to_checkout_new_connection
661
1170
  conn
662
1171
  else
663
1172
  @available.poll(checkout_timeout)
664
1173
  end
665
1174
  end
1175
+ rescue ConnectionTimeoutError => ex
1176
+ raise ex.set_pool(self)
666
1177
  end
667
1178
 
668
1179
  #--
669
- # if owner_thread param is omitted, this must be called in synchronize block
670
- def remove_connection_from_thread_cache(conn, owner_thread = conn.owner)
671
- @thread_cached_conns.delete_pair(connection_cache_key(owner_thread), conn)
672
- end
673
- alias_method :release, :remove_connection_from_thread_cache
1180
+ # If new connections are already being established in the background,
1181
+ # and there are fewer threads already waiting than the number of
1182
+ # upcoming connections, we can just get in queue and wait to be handed a
1183
+ # connection. This avoids us overshooting the required connection count
1184
+ # by starting a new connection ourselves, and is likely to be faster
1185
+ # too (because at least some of the time it takes to establish a new
1186
+ # connection must have already passed).
1187
+ #
1188
+ # If background connections are available, this method will block and
1189
+ # return a connection. If no background connections are available, it
1190
+ # will immediately return +nil+.
1191
+ def try_to_queue_for_background_connection(checkout_timeout)
1192
+ return unless @maintaining > 0
674
1193
 
675
- def new_connection
676
- connection = Base.public_send(db_config.adapter_method, db_config.configuration_hash)
677
- connection.pool = self
1194
+ synchronize do
1195
+ return unless @maintaining > @available.num_waiting
678
1196
 
679
- begin
680
- connection.check_version
681
- rescue
682
- connection.disconnect!
683
- raise
1197
+ # We are guaranteed the "maintaining" thread will return its promised
1198
+ # connection within one maintenance-unit of time. Thus we can safely
1199
+ # do a blocking wait with (functionally) no timeout.
1200
+ @available.poll(100)
684
1201
  end
1202
+ end
685
1203
 
686
- connection
687
- rescue ConnectionNotEstablished => ex
688
- raise ex.set_pool(self)
1204
+ #--
1205
+ # if owner_thread param is omitted, this must be called in synchronize block
1206
+ def remove_connection_from_thread_cache(conn, owner_thread = conn.owner)
1207
+ if owner_thread
1208
+ @leases[owner_thread].clear(conn)
1209
+ end
689
1210
  end
1211
+ alias_method :release, :remove_connection_from_thread_cache
690
1212
 
691
- # If the pool is not at a <tt>@size</tt> limit, establish new connection. Connecting
1213
+ # If the pool is not at a <tt>@max_connections</tt> limit, establish new connection. Connecting
692
1214
  # to the DB is done outside main synchronized section.
1215
+ #
1216
+ # If a block is supplied, it is an additional constraint (checked while holding the
1217
+ # pool lock) on whether a new connection should be established.
693
1218
  #--
694
1219
  # Implementation constraint: a newly established connection returned by this
695
1220
  # method must be in the +.leased+ state.
696
1221
  def try_to_checkout_new_connection
697
1222
  # first in synchronized section check if establishing new conns is allowed
698
- # and increment @now_connecting, to prevent overstepping this pool's @size
1223
+ # and increment @now_connecting, to prevent overstepping this pool's @max_connections
699
1224
  # constraint
700
1225
  do_checkout = synchronize do
701
- if @threads_blocking_new_connections.zero? && (@connections.size + @now_connecting) < @size
1226
+ return if self.discarded?
1227
+
1228
+ if @threads_blocking_new_connections.zero? && (@max_connections.nil? || (@connections.size + @now_connecting) < @max_connections) && (!block_given? || yield)
1229
+ if @connections.size > 0 || @original_context != ActiveSupport::IsolatedExecutionState.context
1230
+ @activated = true
1231
+ end
1232
+
702
1233
  @now_connecting += 1
703
1234
  end
704
1235
  end
@@ -709,12 +1240,16 @@ module ActiveRecord
709
1240
  conn = checkout_new_connection
710
1241
  ensure
711
1242
  synchronize do
1243
+ @now_connecting -= 1
712
1244
  if conn
713
- adopt_connection(conn)
714
- # returned conn needs to be already leased
715
- conn.lease
1245
+ if self.discarded?
1246
+ conn.discard!
1247
+ else
1248
+ adopt_connection(conn)
1249
+ # returned conn needs to be already leased
1250
+ conn.lease
1251
+ end
716
1252
  end
717
- @now_connecting -= 1
718
1253
  end
719
1254
  end
720
1255
  end
@@ -723,6 +1258,12 @@ module ActiveRecord
723
1258
  def adopt_connection(conn)
724
1259
  conn.pool = self
725
1260
  @connections << conn
1261
+
1262
+ # We just created the first connection, it's time to load the schema
1263
+ # cache if that wasn't eagerly done before
1264
+ if @schema_cache.nil? && ActiveRecord.lazily_load_schema_cache
1265
+ schema_cache.load!
1266
+ end
726
1267
  end
727
1268
 
728
1269
  def checkout_new_connection
@@ -731,15 +1272,20 @@ module ActiveRecord
731
1272
  end
732
1273
 
733
1274
  def checkout_and_verify(c)
734
- c._run_checkout_callbacks do
735
- c.clean!
736
- end
737
- c
1275
+ c.clean!
738
1276
  rescue Exception
739
1277
  remove c
740
1278
  c.disconnect!
741
1279
  raise
742
1280
  end
1281
+
1282
+ def name_inspect
1283
+ db_config.name.inspect unless db_config.name == "primary"
1284
+ end
1285
+
1286
+ def shard_inspect
1287
+ shard.inspect unless shard == :default
1288
+ end
743
1289
  end
744
1290
  end
745
1291
  end