activerecord 6.1.7.4 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (238) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1055 -1170
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/active_record/aggregations.rb +1 -1
  6. data/lib/active_record/association_relation.rb +0 -10
  7. data/lib/active_record/associations/association.rb +33 -17
  8. data/lib/active_record/associations/association_scope.rb +1 -3
  9. data/lib/active_record/associations/belongs_to_association.rb +15 -4
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +10 -2
  11. data/lib/active_record/associations/builder/association.rb +8 -2
  12. data/lib/active_record/associations/builder/belongs_to.rb +19 -6
  13. data/lib/active_record/associations/builder/collection_association.rb +10 -3
  14. data/lib/active_record/associations/builder/has_many.rb +3 -2
  15. data/lib/active_record/associations/builder/has_one.rb +2 -1
  16. data/lib/active_record/associations/builder/singular_association.rb +2 -2
  17. data/lib/active_record/associations/collection_association.rb +18 -19
  18. data/lib/active_record/associations/collection_proxy.rb +8 -3
  19. data/lib/active_record/associations/disable_joins_association_scope.rb +59 -0
  20. data/lib/active_record/associations/has_many_association.rb +1 -1
  21. data/lib/active_record/associations/has_many_through_association.rb +2 -1
  22. data/lib/active_record/associations/has_one_association.rb +10 -7
  23. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  24. data/lib/active_record/associations/join_dependency.rb +6 -2
  25. data/lib/active_record/associations/preloader/association.rb +186 -52
  26. data/lib/active_record/associations/preloader/batch.rb +48 -0
  27. data/lib/active_record/associations/preloader/branch.rb +147 -0
  28. data/lib/active_record/associations/preloader/through_association.rb +49 -13
  29. data/lib/active_record/associations/preloader.rb +39 -113
  30. data/lib/active_record/associations/singular_association.rb +8 -2
  31. data/lib/active_record/associations/through_association.rb +3 -3
  32. data/lib/active_record/associations.rb +90 -82
  33. data/lib/active_record/asynchronous_queries_tracker.rb +60 -0
  34. data/lib/active_record/attribute_assignment.rb +1 -1
  35. data/lib/active_record/attribute_methods/before_type_cast.rb +7 -2
  36. data/lib/active_record/attribute_methods/dirty.rb +49 -16
  37. data/lib/active_record/attribute_methods/primary_key.rb +2 -2
  38. data/lib/active_record/attribute_methods/query.rb +2 -2
  39. data/lib/active_record/attribute_methods/read.rb +7 -5
  40. data/lib/active_record/attribute_methods/serialization.rb +66 -12
  41. data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -3
  42. data/lib/active_record/attribute_methods/write.rb +7 -10
  43. data/lib/active_record/attribute_methods.rb +13 -14
  44. data/lib/active_record/attributes.rb +24 -35
  45. data/lib/active_record/autosave_association.rb +6 -21
  46. data/lib/active_record/base.rb +19 -1
  47. data/lib/active_record/callbacks.rb +2 -2
  48. data/lib/active_record/coders/yaml_column.rb +2 -14
  49. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +292 -0
  50. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +209 -0
  51. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +76 -0
  52. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +47 -561
  53. data/lib/active_record/connection_adapters/abstract/database_limits.rb +0 -17
  54. data/lib/active_record/connection_adapters/abstract/database_statements.rb +46 -22
  55. data/lib/active_record/connection_adapters/abstract/query_cache.rb +24 -12
  56. data/lib/active_record/connection_adapters/abstract/quoting.rb +43 -82
  57. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -17
  58. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +34 -13
  59. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +69 -18
  60. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -22
  61. data/lib/active_record/connection_adapters/abstract_adapter.rb +149 -74
  62. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +97 -81
  63. data/lib/active_record/connection_adapters/column.rb +4 -0
  64. data/lib/active_record/connection_adapters/mysql/database_statements.rb +35 -23
  65. data/lib/active_record/connection_adapters/mysql/quoting.rb +35 -21
  66. data/lib/active_record/connection_adapters/mysql2_adapter.rb +12 -6
  67. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  68. data/lib/active_record/connection_adapters/postgresql/column.rb +17 -1
  69. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +19 -12
  70. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +8 -0
  71. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +5 -0
  72. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +53 -14
  73. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +1 -1
  74. data/lib/active_record/connection_adapters/postgresql/oid/timestamp.rb +15 -0
  75. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +28 -0
  76. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +18 -6
  77. data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
  78. data/lib/active_record/connection_adapters/postgresql/quoting.rb +50 -76
  79. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +32 -0
  80. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +21 -1
  81. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +22 -1
  82. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +25 -0
  83. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +27 -16
  84. data/lib/active_record/connection_adapters/postgresql_adapter.rb +207 -107
  85. data/lib/active_record/connection_adapters/schema_cache.rb +39 -38
  86. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +25 -19
  87. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +15 -16
  88. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -2
  89. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +61 -30
  90. data/lib/active_record/connection_adapters.rb +6 -5
  91. data/lib/active_record/connection_handling.rb +47 -53
  92. data/lib/active_record/core.rb +121 -146
  93. data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -1
  94. data/lib/active_record/database_configurations/database_config.rb +12 -9
  95. data/lib/active_record/database_configurations/hash_config.rb +63 -5
  96. data/lib/active_record/database_configurations/url_config.rb +2 -2
  97. data/lib/active_record/database_configurations.rb +15 -32
  98. data/lib/active_record/delegated_type.rb +52 -11
  99. data/lib/active_record/destroy_association_async_job.rb +1 -1
  100. data/lib/active_record/disable_joins_association_relation.rb +39 -0
  101. data/lib/active_record/dynamic_matchers.rb +1 -1
  102. data/lib/active_record/encryption/cipher/aes256_gcm.rb +98 -0
  103. data/lib/active_record/encryption/cipher.rb +53 -0
  104. data/lib/active_record/encryption/config.rb +44 -0
  105. data/lib/active_record/encryption/configurable.rb +61 -0
  106. data/lib/active_record/encryption/context.rb +35 -0
  107. data/lib/active_record/encryption/contexts.rb +72 -0
  108. data/lib/active_record/encryption/derived_secret_key_provider.rb +12 -0
  109. data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
  110. data/lib/active_record/encryption/encryptable_record.rb +208 -0
  111. data/lib/active_record/encryption/encrypted_attribute_type.rb +140 -0
  112. data/lib/active_record/encryption/encrypted_fixtures.rb +38 -0
  113. data/lib/active_record/encryption/encrypting_only_encryptor.rb +12 -0
  114. data/lib/active_record/encryption/encryptor.rb +155 -0
  115. data/lib/active_record/encryption/envelope_encryption_key_provider.rb +55 -0
  116. data/lib/active_record/encryption/errors.rb +15 -0
  117. data/lib/active_record/encryption/extended_deterministic_queries.rb +160 -0
  118. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +28 -0
  119. data/lib/active_record/encryption/key.rb +28 -0
  120. data/lib/active_record/encryption/key_generator.rb +42 -0
  121. data/lib/active_record/encryption/key_provider.rb +46 -0
  122. data/lib/active_record/encryption/message.rb +33 -0
  123. data/lib/active_record/encryption/message_serializer.rb +90 -0
  124. data/lib/active_record/encryption/null_encryptor.rb +21 -0
  125. data/lib/active_record/encryption/properties.rb +76 -0
  126. data/lib/active_record/encryption/read_only_null_encryptor.rb +24 -0
  127. data/lib/active_record/encryption/scheme.rb +99 -0
  128. data/lib/active_record/encryption.rb +55 -0
  129. data/lib/active_record/enum.rb +49 -42
  130. data/lib/active_record/errors.rb +67 -4
  131. data/lib/active_record/explain_registry.rb +11 -6
  132. data/lib/active_record/fixture_set/file.rb +15 -1
  133. data/lib/active_record/fixture_set/table_row.rb +41 -6
  134. data/lib/active_record/fixture_set/table_rows.rb +4 -4
  135. data/lib/active_record/fixtures.rb +17 -20
  136. data/lib/active_record/future_result.rb +139 -0
  137. data/lib/active_record/gem_version.rb +4 -4
  138. data/lib/active_record/inheritance.rb +55 -17
  139. data/lib/active_record/insert_all.rb +80 -14
  140. data/lib/active_record/integration.rb +4 -3
  141. data/lib/active_record/internal_metadata.rb +1 -5
  142. data/lib/active_record/legacy_yaml_adapter.rb +2 -39
  143. data/lib/active_record/locking/optimistic.rb +10 -9
  144. data/lib/active_record/locking/pessimistic.rb +9 -3
  145. data/lib/active_record/log_subscriber.rb +14 -3
  146. data/lib/active_record/middleware/database_selector/resolver.rb +6 -10
  147. data/lib/active_record/middleware/database_selector.rb +8 -3
  148. data/lib/active_record/middleware/shard_selector.rb +60 -0
  149. data/lib/active_record/migration/command_recorder.rb +4 -4
  150. data/lib/active_record/migration/compatibility.rb +89 -10
  151. data/lib/active_record/migration/join_table.rb +1 -1
  152. data/lib/active_record/migration.rb +110 -80
  153. data/lib/active_record/model_schema.rb +45 -58
  154. data/lib/active_record/nested_attributes.rb +13 -12
  155. data/lib/active_record/no_touching.rb +3 -3
  156. data/lib/active_record/null_relation.rb +2 -6
  157. data/lib/active_record/persistence.rb +219 -52
  158. data/lib/active_record/query_cache.rb +2 -2
  159. data/lib/active_record/query_logs.rb +138 -0
  160. data/lib/active_record/querying.rb +15 -5
  161. data/lib/active_record/railtie.rb +127 -17
  162. data/lib/active_record/railties/controller_runtime.rb +1 -1
  163. data/lib/active_record/railties/databases.rake +66 -129
  164. data/lib/active_record/readonly_attributes.rb +11 -0
  165. data/lib/active_record/reflection.rb +67 -50
  166. data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
  167. data/lib/active_record/relation/batches.rb +3 -3
  168. data/lib/active_record/relation/calculations.rb +40 -36
  169. data/lib/active_record/relation/delegation.rb +6 -6
  170. data/lib/active_record/relation/finder_methods.rb +31 -35
  171. data/lib/active_record/relation/merger.rb +20 -13
  172. data/lib/active_record/relation/predicate_builder.rb +1 -6
  173. data/lib/active_record/relation/query_attribute.rb +5 -11
  174. data/lib/active_record/relation/query_methods.rb +235 -63
  175. data/lib/active_record/relation/record_fetch_warning.rb +7 -9
  176. data/lib/active_record/relation/spawn_methods.rb +2 -2
  177. data/lib/active_record/relation/where_clause.rb +10 -19
  178. data/lib/active_record/relation.rb +169 -84
  179. data/lib/active_record/result.rb +17 -7
  180. data/lib/active_record/runtime_registry.rb +9 -13
  181. data/lib/active_record/sanitization.rb +11 -7
  182. data/lib/active_record/schema_dumper.rb +10 -3
  183. data/lib/active_record/schema_migration.rb +4 -4
  184. data/lib/active_record/scoping/default.rb +61 -12
  185. data/lib/active_record/scoping/named.rb +3 -11
  186. data/lib/active_record/scoping.rb +64 -34
  187. data/lib/active_record/serialization.rb +1 -1
  188. data/lib/active_record/signed_id.rb +1 -1
  189. data/lib/active_record/store.rb +1 -6
  190. data/lib/active_record/suppressor.rb +11 -15
  191. data/lib/active_record/tasks/database_tasks.rb +116 -58
  192. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  193. data/lib/active_record/tasks/postgresql_database_tasks.rb +19 -12
  194. data/lib/active_record/test_databases.rb +1 -1
  195. data/lib/active_record/test_fixtures.rb +9 -13
  196. data/lib/active_record/timestamp.rb +3 -4
  197. data/lib/active_record/transactions.rb +9 -14
  198. data/lib/active_record/translation.rb +2 -2
  199. data/lib/active_record/type/adapter_specific_registry.rb +32 -7
  200. data/lib/active_record/type/hash_lookup_type_map.rb +34 -1
  201. data/lib/active_record/type/internal/timezone.rb +2 -2
  202. data/lib/active_record/type/serialized.rb +1 -1
  203. data/lib/active_record/type/type_map.rb +17 -20
  204. data/lib/active_record/type.rb +1 -2
  205. data/lib/active_record/validations/associated.rb +1 -1
  206. data/lib/active_record/validations/uniqueness.rb +1 -1
  207. data/lib/active_record.rb +204 -28
  208. data/lib/arel/attributes/attribute.rb +0 -8
  209. data/lib/arel/crud.rb +28 -22
  210. data/lib/arel/delete_manager.rb +18 -4
  211. data/lib/arel/filter_predications.rb +9 -0
  212. data/lib/arel/insert_manager.rb +2 -3
  213. data/lib/arel/nodes/casted.rb +1 -1
  214. data/lib/arel/nodes/delete_statement.rb +12 -13
  215. data/lib/arel/nodes/filter.rb +10 -0
  216. data/lib/arel/nodes/function.rb +1 -0
  217. data/lib/arel/nodes/insert_statement.rb +2 -2
  218. data/lib/arel/nodes/select_core.rb +2 -2
  219. data/lib/arel/nodes/select_statement.rb +2 -2
  220. data/lib/arel/nodes/update_statement.rb +8 -3
  221. data/lib/arel/nodes.rb +1 -0
  222. data/lib/arel/predications.rb +11 -3
  223. data/lib/arel/select_manager.rb +10 -4
  224. data/lib/arel/table.rb +0 -1
  225. data/lib/arel/tree_manager.rb +0 -12
  226. data/lib/arel/update_manager.rb +18 -4
  227. data/lib/arel/visitors/dot.rb +80 -90
  228. data/lib/arel/visitors/mysql.rb +8 -2
  229. data/lib/arel/visitors/postgresql.rb +0 -10
  230. data/lib/arel/visitors/to_sql.rb +58 -2
  231. data/lib/arel.rb +2 -1
  232. data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
  233. data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +1 -1
  234. data/lib/rails/generators/active_record/model/templates/model.rb.tt +1 -1
  235. data/lib/rails/generators/active_record/model/templates/module.rb.tt +2 -2
  236. data/lib/rails/generators/active_record/multi_db/multi_db_generator.rb +16 -0
  237. data/lib/rails/generators/active_record/multi_db/templates/multi_db.rb.tt +44 -0
  238. metadata +58 -14
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thread"
4
+ require "concurrent/map"
5
+
6
+ module ActiveRecord
7
+ module ConnectionAdapters
8
+ # ConnectionHandler is a collection of ConnectionPool objects. It is used
9
+ # for keeping separate connection pools that connect to different databases.
10
+ #
11
+ # For example, suppose that you have 5 models, with the following hierarchy:
12
+ #
13
+ # class Author < ActiveRecord::Base
14
+ # end
15
+ #
16
+ # class BankAccount < ActiveRecord::Base
17
+ # end
18
+ #
19
+ # class Book < ActiveRecord::Base
20
+ # establish_connection :library_db
21
+ # end
22
+ #
23
+ # class ScaryBook < Book
24
+ # end
25
+ #
26
+ # class GoodBook < Book
27
+ # end
28
+ #
29
+ # And a database.yml that looked like this:
30
+ #
31
+ # development:
32
+ # database: my_application
33
+ # host: localhost
34
+ #
35
+ # library_db:
36
+ # database: library
37
+ # host: some.library.org
38
+ #
39
+ # Your primary database in the development environment is "my_application"
40
+ # but the Book model connects to a separate database called "library_db"
41
+ # (this can even be a database on a different machine).
42
+ #
43
+ # Book, ScaryBook and GoodBook will all use the same connection pool to
44
+ # "library_db" while Author, BankAccount, and any other models you create
45
+ # will use the default connection pool to "my_application".
46
+ #
47
+ # The various connection pools are managed by a single instance of
48
+ # ConnectionHandler accessible via ActiveRecord::Base.connection_handler.
49
+ # All Active Record models use this handler to determine the connection pool that they
50
+ # should use.
51
+ #
52
+ # The ConnectionHandler class is not coupled with the Active models, as it has no knowledge
53
+ # about the model. The model needs to pass a connection specification name to the handler,
54
+ # in order to look up the correct connection pool.
55
+ class ConnectionHandler
56
+ FINALIZER = lambda { |_| ActiveSupport::ForkTracker.check! }
57
+ private_constant :FINALIZER
58
+
59
+ class StringConnectionOwner # :nodoc:
60
+ attr_reader :name
61
+
62
+ def initialize(name)
63
+ @name = name
64
+ end
65
+
66
+ def primary_class?
67
+ false
68
+ end
69
+
70
+ def current_preventing_writes
71
+ false
72
+ end
73
+ end
74
+
75
+ def initialize
76
+ # These caches are keyed by pool_config.connection_specification_name (PoolConfig#connection_specification_name).
77
+ @owner_to_pool_manager = Concurrent::Map.new(initial_capacity: 2)
78
+
79
+ # Backup finalizer: if the forked child skipped Kernel#fork the early discard has not occurred
80
+ ObjectSpace.define_finalizer self, FINALIZER
81
+ end
82
+
83
+ def prevent_writes # :nodoc:
84
+ ActiveSupport::IsolatedExecutionState[:active_record_prevent_writes]
85
+ end
86
+
87
+ def prevent_writes=(prevent_writes) # :nodoc:
88
+ ActiveSupport::IsolatedExecutionState[:active_record_prevent_writes] = prevent_writes
89
+ end
90
+
91
+ # Prevent writing to the database regardless of role.
92
+ #
93
+ # In some cases you may want to prevent writes to the database
94
+ # even if you are on a database that can write. +while_preventing_writes+
95
+ # will prevent writes to the database for the duration of the block.
96
+ #
97
+ # This method does not provide the same protection as a readonly
98
+ # user and is meant to be a safeguard against accidental writes.
99
+ #
100
+ # See +READ_QUERY+ for the queries that are blocked by this
101
+ # method.
102
+ def while_preventing_writes(enabled = true)
103
+ unless ActiveRecord.legacy_connection_handling
104
+ raise NotImplementedError, "`while_preventing_writes` is only available on the connection_handler with legacy_connection_handling"
105
+ end
106
+
107
+ original, self.prevent_writes = self.prevent_writes, enabled
108
+ yield
109
+ ensure
110
+ self.prevent_writes = original
111
+ end
112
+
113
+ def connection_pool_names # :nodoc:
114
+ owner_to_pool_manager.keys
115
+ end
116
+
117
+ def all_connection_pools
118
+ owner_to_pool_manager.values.flat_map { |m| m.pool_configs.map(&:pool) }
119
+ end
120
+
121
+ def connection_pool_list(role = ActiveRecord::Base.current_role)
122
+ owner_to_pool_manager.values.flat_map { |m| m.pool_configs(role).map(&:pool) }
123
+ end
124
+ alias :connection_pools :connection_pool_list
125
+
126
+ def establish_connection(config, owner_name: Base, role: ActiveRecord::Base.current_role, shard: Base.current_shard)
127
+ owner_name = StringConnectionOwner.new(config.to_s) if config.is_a?(Symbol)
128
+
129
+ pool_config = resolve_pool_config(config, owner_name, role, shard)
130
+ db_config = pool_config.db_config
131
+
132
+ # Protects the connection named `ActiveRecord::Base` from being removed
133
+ # if the user calls `establish_connection :primary`.
134
+ if owner_to_pool_manager.key?(pool_config.connection_specification_name)
135
+ remove_connection_pool(pool_config.connection_specification_name, role: role, shard: shard)
136
+ end
137
+
138
+ message_bus = ActiveSupport::Notifications.instrumenter
139
+ payload = {}
140
+ if pool_config
141
+ payload[:spec_name] = pool_config.connection_specification_name
142
+ payload[:shard] = shard
143
+ payload[:config] = db_config.configuration_hash
144
+ end
145
+
146
+ if ActiveRecord.legacy_connection_handling
147
+ owner_to_pool_manager[pool_config.connection_specification_name] ||= LegacyPoolManager.new
148
+ else
149
+ owner_to_pool_manager[pool_config.connection_specification_name] ||= PoolManager.new
150
+ end
151
+ pool_manager = get_pool_manager(pool_config.connection_specification_name)
152
+ pool_manager.set_pool_config(role, shard, pool_config)
153
+
154
+ message_bus.instrument("!connection.active_record", payload) do
155
+ pool_config.pool
156
+ end
157
+ end
158
+
159
+ # Returns true if there are any active connections among the connection
160
+ # pools that the ConnectionHandler is managing.
161
+ def active_connections?(role = ActiveRecord::Base.current_role)
162
+ connection_pool_list(role).any?(&:active_connection?)
163
+ end
164
+
165
+ # Returns any connections in use by the current thread back to the pool,
166
+ # and also returns connections to the pool cached by threads that are no
167
+ # longer alive.
168
+ def clear_active_connections!(role = ActiveRecord::Base.current_role)
169
+ connection_pool_list(role).each(&:release_connection)
170
+ end
171
+
172
+ # Clears the cache which maps classes.
173
+ #
174
+ # See ConnectionPool#clear_reloadable_connections! for details.
175
+ def clear_reloadable_connections!(role = ActiveRecord::Base.current_role)
176
+ connection_pool_list(role).each(&:clear_reloadable_connections!)
177
+ end
178
+
179
+ def clear_all_connections!(role = ActiveRecord::Base.current_role)
180
+ connection_pool_list(role).each(&:disconnect!)
181
+ end
182
+
183
+ # Disconnects all currently idle connections.
184
+ #
185
+ # See ConnectionPool#flush! for details.
186
+ def flush_idle_connections!(role = ActiveRecord::Base.current_role)
187
+ connection_pool_list(role).each(&:flush!)
188
+ end
189
+
190
+ # Locate the connection of the nearest super class. This can be an
191
+ # active or defined connection: if it is the latter, it will be
192
+ # opened and set as the active connection for the class it was defined
193
+ # for (not necessarily the current class).
194
+ def retrieve_connection(spec_name, role: ActiveRecord::Base.current_role, shard: ActiveRecord::Base.current_shard) # :nodoc:
195
+ pool = retrieve_connection_pool(spec_name, role: role, shard: shard)
196
+
197
+ unless pool
198
+ if shard != ActiveRecord::Base.default_shard
199
+ message = "No connection pool for '#{spec_name}' found for the '#{shard}' shard."
200
+ elsif ActiveRecord::Base.connection_handler != ActiveRecord::Base.default_connection_handler
201
+ message = "No connection pool for '#{spec_name}' found for the '#{ActiveRecord::Base.current_role}' role."
202
+ elsif role != ActiveRecord::Base.default_role
203
+ message = "No connection pool for '#{spec_name}' found for the '#{role}' role."
204
+ else
205
+ message = "No connection pool for '#{spec_name}' found."
206
+ end
207
+
208
+ raise ConnectionNotEstablished, message
209
+ end
210
+
211
+ pool.connection
212
+ end
213
+
214
+ # Returns true if a connection that's accessible to this class has
215
+ # already been opened.
216
+ def connected?(spec_name, role: ActiveRecord::Base.current_role, shard: ActiveRecord::Base.current_shard)
217
+ pool = retrieve_connection_pool(spec_name, role: role, shard: shard)
218
+ pool && pool.connected?
219
+ end
220
+
221
+ def remove_connection_pool(owner, role: ActiveRecord::Base.current_role, shard: ActiveRecord::Base.current_shard)
222
+ if pool_manager = get_pool_manager(owner)
223
+ pool_config = pool_manager.remove_pool_config(role, shard)
224
+
225
+ if pool_config
226
+ pool_config.disconnect!
227
+ pool_config.db_config
228
+ end
229
+ end
230
+ end
231
+
232
+ # Retrieving the connection pool happens a lot, so we cache it in @owner_to_pool_manager.
233
+ # This makes retrieving the connection pool O(1) once the process is warm.
234
+ # When a connection is established or removed, we invalidate the cache.
235
+ def retrieve_connection_pool(owner, role: ActiveRecord::Base.current_role, shard: ActiveRecord::Base.current_shard)
236
+ pool_config = get_pool_manager(owner)&.get_pool_config(role, shard)
237
+ pool_config&.pool
238
+ end
239
+
240
+ private
241
+ attr_reader :owner_to_pool_manager
242
+
243
+ # Returns the pool manager for an owner.
244
+ def get_pool_manager(owner)
245
+ owner_to_pool_manager[owner]
246
+ end
247
+
248
+ # Returns an instance of PoolConfig for a given adapter.
249
+ # Accepts a hash one layer deep that contains all connection information.
250
+ #
251
+ # == Example
252
+ #
253
+ # config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
254
+ # pool_config = Base.configurations.resolve_pool_config(:production)
255
+ # pool_config.db_config.configuration_hash
256
+ # # => { host: "localhost", database: "foo", adapter: "sqlite3" }
257
+ #
258
+ def resolve_pool_config(config, owner_name, role, shard)
259
+ db_config = Base.configurations.resolve(config)
260
+
261
+ raise(AdapterNotSpecified, "database configuration does not specify adapter") unless db_config.adapter
262
+
263
+ # Require the adapter itself and give useful feedback about
264
+ # 1. Missing adapter gems and
265
+ # 2. Adapter gems' missing dependencies.
266
+ path_to_adapter = "active_record/connection_adapters/#{db_config.adapter}_adapter"
267
+ begin
268
+ require path_to_adapter
269
+ rescue LoadError => e
270
+ # We couldn't require the adapter itself. Raise an exception that
271
+ # points out config typos and missing gems.
272
+ if e.path == path_to_adapter
273
+ # We can assume that a non-builtin adapter was specified, so it's
274
+ # either misspelled or missing from Gemfile.
275
+ raise LoadError, "Could not load the '#{db_config.adapter}' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
276
+
277
+ # Bubbled up from the adapter require. Prefix the exception message
278
+ # with some guidance about how to address it and reraise.
279
+ else
280
+ raise LoadError, "Error loading the '#{db_config.adapter}' Active Record adapter. Missing a gem it depends on? #{e.message}", e.backtrace
281
+ end
282
+ end
283
+
284
+ unless ActiveRecord::Base.respond_to?(db_config.adapter_method)
285
+ raise AdapterNotFound, "database configuration specifies nonexistent #{db_config.adapter} adapter"
286
+ end
287
+
288
+ ConnectionAdapters::PoolConfig.new(owner_name, db_config, role, shard)
289
+ end
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thread"
4
+ require "monitor"
5
+
6
+ module ActiveRecord
7
+ module ConnectionAdapters
8
+ class ConnectionPool
9
+ # Threadsafe, fair, LIFO queue. Meant to be used by ConnectionPool
10
+ # with which it shares a Monitor.
11
+ class Queue
12
+ def initialize(lock = Monitor.new)
13
+ @lock = lock
14
+ @cond = @lock.new_cond
15
+ @num_waiting = 0
16
+ @queue = []
17
+ end
18
+
19
+ # Test if any threads are currently waiting on the queue.
20
+ def any_waiting?
21
+ synchronize do
22
+ @num_waiting > 0
23
+ end
24
+ end
25
+
26
+ # Returns the number of threads currently waiting on this
27
+ # queue.
28
+ def num_waiting
29
+ synchronize do
30
+ @num_waiting
31
+ end
32
+ end
33
+
34
+ # Add +element+ to the queue. Never blocks.
35
+ def add(element)
36
+ synchronize do
37
+ @queue.push element
38
+ @cond.signal
39
+ end
40
+ end
41
+
42
+ # If +element+ is in the queue, remove and return it, or +nil+.
43
+ def delete(element)
44
+ synchronize do
45
+ @queue.delete(element)
46
+ end
47
+ end
48
+
49
+ # Remove all elements from the queue.
50
+ def clear
51
+ synchronize do
52
+ @queue.clear
53
+ end
54
+ end
55
+
56
+ # Remove the head of the queue.
57
+ #
58
+ # If +timeout+ is not given, remove and return the head of the
59
+ # queue if the number of available elements is strictly
60
+ # greater than the number of threads currently waiting (that
61
+ # is, don't jump ahead in line). Otherwise, return +nil+.
62
+ #
63
+ # If +timeout+ is given, block if there is no element
64
+ # available, waiting up to +timeout+ seconds for an element to
65
+ # become available.
66
+ #
67
+ # Raises:
68
+ # - ActiveRecord::ConnectionTimeoutError if +timeout+ is given and no element
69
+ # becomes available within +timeout+ seconds,
70
+ def poll(timeout = nil)
71
+ synchronize { internal_poll(timeout) }
72
+ end
73
+
74
+ private
75
+ def internal_poll(timeout)
76
+ no_wait_poll || (timeout && wait_poll(timeout))
77
+ end
78
+
79
+ def synchronize(&block)
80
+ @lock.synchronize(&block)
81
+ end
82
+
83
+ # Test if the queue currently contains any elements.
84
+ def any?
85
+ !@queue.empty?
86
+ end
87
+
88
+ # A thread can remove an element from the queue without
89
+ # waiting if and only if the number of currently available
90
+ # connections is strictly greater than the number of waiting
91
+ # threads.
92
+ def can_remove_no_wait?
93
+ @queue.size > @num_waiting
94
+ end
95
+
96
+ # Removes and returns the head of the queue if possible, or +nil+.
97
+ def remove
98
+ @queue.pop
99
+ end
100
+
101
+ # Remove and return the head of the queue if the number of
102
+ # available elements is strictly greater than the number of
103
+ # threads currently waiting. Otherwise, return +nil+.
104
+ def no_wait_poll
105
+ remove if can_remove_no_wait?
106
+ end
107
+
108
+ # Waits on the queue up to +timeout+ seconds, then removes and
109
+ # returns the head of the queue.
110
+ def wait_poll(timeout)
111
+ @num_waiting += 1
112
+
113
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
114
+ elapsed = 0
115
+ loop do
116
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
117
+ @cond.wait(timeout - elapsed)
118
+ end
119
+
120
+ return remove if any?
121
+
122
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
123
+ if elapsed >= timeout
124
+ msg = "could not obtain a connection from the pool within %0.3f seconds (waited %0.3f seconds); all pooled connections were in use" %
125
+ [timeout, elapsed]
126
+ raise ConnectionTimeoutError, msg
127
+ end
128
+ end
129
+ ensure
130
+ @num_waiting -= 1
131
+ end
132
+ end
133
+
134
+ # Adds the ability to turn a basic fair FIFO queue into one
135
+ # biased to some thread.
136
+ module BiasableQueue # :nodoc:
137
+ class BiasedConditionVariable # :nodoc:
138
+ # semantics of condition variables guarantee that +broadcast+, +broadcast_on_biased+,
139
+ # +signal+ and +wait+ methods are only called while holding a lock
140
+ def initialize(lock, other_cond, preferred_thread)
141
+ @real_cond = lock.new_cond
142
+ @other_cond = other_cond
143
+ @preferred_thread = preferred_thread
144
+ @num_waiting_on_real_cond = 0
145
+ end
146
+
147
+ def broadcast
148
+ broadcast_on_biased
149
+ @other_cond.broadcast
150
+ end
151
+
152
+ def broadcast_on_biased
153
+ @num_waiting_on_real_cond = 0
154
+ @real_cond.broadcast
155
+ end
156
+
157
+ def signal
158
+ if @num_waiting_on_real_cond > 0
159
+ @num_waiting_on_real_cond -= 1
160
+ @real_cond
161
+ else
162
+ @other_cond
163
+ end.signal
164
+ end
165
+
166
+ def wait(timeout)
167
+ if Thread.current == @preferred_thread
168
+ @num_waiting_on_real_cond += 1
169
+ @real_cond
170
+ else
171
+ @other_cond
172
+ end.wait(timeout)
173
+ end
174
+ end
175
+
176
+ def with_a_bias_for(thread)
177
+ previous_cond = nil
178
+ new_cond = nil
179
+ synchronize do
180
+ previous_cond = @cond
181
+ @cond = new_cond = BiasedConditionVariable.new(@lock, @cond, thread)
182
+ end
183
+ yield
184
+ ensure
185
+ synchronize do
186
+ @cond = previous_cond if previous_cond
187
+ new_cond.broadcast_on_biased if new_cond # wake up any remaining sleepers
188
+ end
189
+ end
190
+ end
191
+
192
+ # Connections must be leased while holding the main pool mutex. This is
193
+ # an internal subclass that also +.leases+ returned connections while
194
+ # still in queue's critical section (queue synchronizes with the same
195
+ # <tt>@lock</tt> as the main pool) so that a returned connection is already
196
+ # leased and there is no need to re-enter synchronized block.
197
+ class ConnectionLeasingQueue < Queue # :nodoc:
198
+ include BiasableQueue
199
+
200
+ private
201
+ def internal_poll(timeout)
202
+ conn = super
203
+ conn.lease if conn
204
+ conn
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thread"
4
+ require "weakref"
5
+
6
+ module ActiveRecord
7
+ module ConnectionAdapters
8
+ class ConnectionPool
9
+ # Every +frequency+ seconds, the reaper will call +reap+ and +flush+ on
10
+ # +pool+. A reaper instantiated with a zero frequency will never reap
11
+ # the connection pool.
12
+ #
13
+ # Configure the frequency by setting +reaping_frequency+ in your database
14
+ # yaml file (default 60 seconds).
15
+ class Reaper
16
+ attr_reader :pool, :frequency
17
+
18
+ def initialize(pool, frequency)
19
+ @pool = pool
20
+ @frequency = frequency
21
+ end
22
+
23
+ @mutex = Mutex.new
24
+ @pools = {}
25
+ @threads = {}
26
+
27
+ class << self
28
+ def register_pool(pool, frequency) # :nodoc:
29
+ @mutex.synchronize do
30
+ unless @threads[frequency]&.alive?
31
+ @threads[frequency] = spawn_thread(frequency)
32
+ end
33
+ @pools[frequency] ||= []
34
+ @pools[frequency] << WeakRef.new(pool)
35
+ end
36
+ end
37
+
38
+ private
39
+ def spawn_thread(frequency)
40
+ Thread.new(frequency) do |t|
41
+ # Advise multi-threaded app servers to ignore this thread for
42
+ # the purposes of fork safety warnings
43
+ Thread.current.thread_variable_set(:fork_safe, true)
44
+ running = true
45
+ while running
46
+ sleep t
47
+ @mutex.synchronize do
48
+ @pools[frequency].select! do |pool|
49
+ pool.weakref_alive? && !pool.discarded?
50
+ end
51
+
52
+ @pools[frequency].each do |p|
53
+ p.reap
54
+ p.flush
55
+ rescue WeakRef::RefError
56
+ end
57
+
58
+ if @pools[frequency].empty?
59
+ @pools.delete(frequency)
60
+ @threads.delete(frequency)
61
+ running = false
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ def run
70
+ return unless frequency && frequency > 0
71
+ self.class.register_pool(pool, frequency)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end