activerecord 6.1.7.2 → 7.0.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (243) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1295 -1007
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/aggregations.rb +1 -1
  5. data/lib/active_record/association_relation.rb +0 -10
  6. data/lib/active_record/associations/association.rb +33 -17
  7. data/lib/active_record/associations/association_scope.rb +1 -3
  8. data/lib/active_record/associations/belongs_to_association.rb +15 -4
  9. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +10 -2
  10. data/lib/active_record/associations/builder/association.rb +8 -2
  11. data/lib/active_record/associations/builder/belongs_to.rb +19 -6
  12. data/lib/active_record/associations/builder/collection_association.rb +10 -3
  13. data/lib/active_record/associations/builder/has_many.rb +3 -2
  14. data/lib/active_record/associations/builder/has_one.rb +2 -1
  15. data/lib/active_record/associations/builder/singular_association.rb +2 -2
  16. data/lib/active_record/associations/collection_association.rb +19 -21
  17. data/lib/active_record/associations/collection_proxy.rb +10 -5
  18. data/lib/active_record/associations/disable_joins_association_scope.rb +59 -0
  19. data/lib/active_record/associations/has_many_association.rb +8 -5
  20. data/lib/active_record/associations/has_many_through_association.rb +2 -1
  21. data/lib/active_record/associations/has_one_association.rb +10 -7
  22. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  23. data/lib/active_record/associations/join_dependency.rb +23 -15
  24. data/lib/active_record/associations/preloader/association.rb +186 -52
  25. data/lib/active_record/associations/preloader/batch.rb +48 -0
  26. data/lib/active_record/associations/preloader/branch.rb +147 -0
  27. data/lib/active_record/associations/preloader/through_association.rb +49 -13
  28. data/lib/active_record/associations/preloader.rb +39 -113
  29. data/lib/active_record/associations/singular_association.rb +8 -2
  30. data/lib/active_record/associations/through_association.rb +3 -3
  31. data/lib/active_record/associations.rb +124 -95
  32. data/lib/active_record/asynchronous_queries_tracker.rb +60 -0
  33. data/lib/active_record/attribute_assignment.rb +1 -1
  34. data/lib/active_record/attribute_methods/before_type_cast.rb +7 -2
  35. data/lib/active_record/attribute_methods/dirty.rb +49 -16
  36. data/lib/active_record/attribute_methods/primary_key.rb +2 -2
  37. data/lib/active_record/attribute_methods/query.rb +2 -2
  38. data/lib/active_record/attribute_methods/read.rb +7 -5
  39. data/lib/active_record/attribute_methods/serialization.rb +57 -19
  40. data/lib/active_record/attribute_methods/time_zone_conversion.rb +8 -3
  41. data/lib/active_record/attribute_methods/write.rb +7 -10
  42. data/lib/active_record/attribute_methods.rb +14 -15
  43. data/lib/active_record/attributes.rb +24 -35
  44. data/lib/active_record/autosave_association.rb +8 -23
  45. data/lib/active_record/base.rb +19 -1
  46. data/lib/active_record/callbacks.rb +2 -2
  47. data/lib/active_record/coders/yaml_column.rb +4 -8
  48. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +292 -0
  49. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +209 -0
  50. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +76 -0
  51. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +47 -561
  52. data/lib/active_record/connection_adapters/abstract/database_limits.rb +0 -17
  53. data/lib/active_record/connection_adapters/abstract/database_statements.rb +46 -22
  54. data/lib/active_record/connection_adapters/abstract/query_cache.rb +24 -12
  55. data/lib/active_record/connection_adapters/abstract/quoting.rb +42 -72
  56. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -17
  57. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +38 -13
  58. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +14 -1
  59. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +80 -24
  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 +105 -81
  63. data/lib/active_record/connection_adapters/column.rb +4 -0
  64. data/lib/active_record/connection_adapters/mysql/database_statements.rb +36 -24
  65. data/lib/active_record/connection_adapters/mysql/quoting.rb +37 -21
  66. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +7 -1
  67. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +20 -1
  68. data/lib/active_record/connection_adapters/mysql2_adapter.rb +12 -6
  69. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  70. data/lib/active_record/connection_adapters/postgresql/column.rb +17 -1
  71. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +19 -12
  72. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +8 -0
  73. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +5 -0
  74. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +53 -14
  75. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +1 -1
  76. data/lib/active_record/connection_adapters/postgresql/oid/timestamp.rb +15 -0
  77. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +30 -0
  78. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +18 -6
  79. data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
  80. data/lib/active_record/connection_adapters/postgresql/quoting.rb +71 -71
  81. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +34 -0
  82. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +21 -1
  83. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +22 -1
  84. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +25 -0
  85. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +37 -19
  86. data/lib/active_record/connection_adapters/postgresql_adapter.rb +206 -105
  87. data/lib/active_record/connection_adapters/schema_cache.rb +39 -38
  88. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +25 -19
  89. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +28 -16
  90. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +17 -15
  91. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +96 -32
  92. data/lib/active_record/connection_adapters.rb +6 -5
  93. data/lib/active_record/connection_handling.rb +49 -55
  94. data/lib/active_record/core.rb +123 -148
  95. data/lib/active_record/database_configurations/connection_url_resolver.rb +2 -1
  96. data/lib/active_record/database_configurations/database_config.rb +12 -9
  97. data/lib/active_record/database_configurations/hash_config.rb +63 -5
  98. data/lib/active_record/database_configurations/url_config.rb +2 -2
  99. data/lib/active_record/database_configurations.rb +15 -32
  100. data/lib/active_record/delegated_type.rb +53 -12
  101. data/lib/active_record/destroy_association_async_job.rb +1 -1
  102. data/lib/active_record/disable_joins_association_relation.rb +39 -0
  103. data/lib/active_record/dynamic_matchers.rb +1 -1
  104. data/lib/active_record/encryption/cipher/aes256_gcm.rb +98 -0
  105. data/lib/active_record/encryption/cipher.rb +53 -0
  106. data/lib/active_record/encryption/config.rb +44 -0
  107. data/lib/active_record/encryption/configurable.rb +67 -0
  108. data/lib/active_record/encryption/context.rb +35 -0
  109. data/lib/active_record/encryption/contexts.rb +72 -0
  110. data/lib/active_record/encryption/derived_secret_key_provider.rb +12 -0
  111. data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
  112. data/lib/active_record/encryption/encryptable_record.rb +206 -0
  113. data/lib/active_record/encryption/encrypted_attribute_type.rb +140 -0
  114. data/lib/active_record/encryption/encrypted_fixtures.rb +38 -0
  115. data/lib/active_record/encryption/encrypting_only_encryptor.rb +12 -0
  116. data/lib/active_record/encryption/encryptor.rb +155 -0
  117. data/lib/active_record/encryption/envelope_encryption_key_provider.rb +55 -0
  118. data/lib/active_record/encryption/errors.rb +15 -0
  119. data/lib/active_record/encryption/extended_deterministic_queries.rb +160 -0
  120. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +28 -0
  121. data/lib/active_record/encryption/key.rb +28 -0
  122. data/lib/active_record/encryption/key_generator.rb +42 -0
  123. data/lib/active_record/encryption/key_provider.rb +46 -0
  124. data/lib/active_record/encryption/message.rb +33 -0
  125. data/lib/active_record/encryption/message_serializer.rb +90 -0
  126. data/lib/active_record/encryption/null_encryptor.rb +21 -0
  127. data/lib/active_record/encryption/properties.rb +76 -0
  128. data/lib/active_record/encryption/read_only_null_encryptor.rb +24 -0
  129. data/lib/active_record/encryption/scheme.rb +99 -0
  130. data/lib/active_record/encryption.rb +55 -0
  131. data/lib/active_record/enum.rb +50 -43
  132. data/lib/active_record/errors.rb +67 -4
  133. data/lib/active_record/explain_registry.rb +11 -6
  134. data/lib/active_record/fixture_set/file.rb +15 -1
  135. data/lib/active_record/fixture_set/table_row.rb +41 -6
  136. data/lib/active_record/fixture_set/table_rows.rb +4 -4
  137. data/lib/active_record/fixtures.rb +20 -23
  138. data/lib/active_record/future_result.rb +139 -0
  139. data/lib/active_record/gem_version.rb +4 -4
  140. data/lib/active_record/inheritance.rb +55 -17
  141. data/lib/active_record/insert_all.rb +80 -14
  142. data/lib/active_record/integration.rb +4 -3
  143. data/lib/active_record/internal_metadata.rb +1 -5
  144. data/lib/active_record/legacy_yaml_adapter.rb +2 -39
  145. data/lib/active_record/locking/optimistic.rb +10 -9
  146. data/lib/active_record/locking/pessimistic.rb +10 -4
  147. data/lib/active_record/log_subscriber.rb +23 -7
  148. data/lib/active_record/middleware/database_selector/resolver.rb +6 -10
  149. data/lib/active_record/middleware/database_selector.rb +18 -6
  150. data/lib/active_record/middleware/shard_selector.rb +60 -0
  151. data/lib/active_record/migration/command_recorder.rb +7 -7
  152. data/lib/active_record/migration/compatibility.rb +84 -2
  153. data/lib/active_record/migration/join_table.rb +1 -1
  154. data/lib/active_record/migration.rb +114 -83
  155. data/lib/active_record/model_schema.rb +58 -59
  156. data/lib/active_record/nested_attributes.rb +13 -12
  157. data/lib/active_record/no_touching.rb +3 -3
  158. data/lib/active_record/null_relation.rb +2 -6
  159. data/lib/active_record/persistence.rb +228 -60
  160. data/lib/active_record/query_cache.rb +2 -2
  161. data/lib/active_record/query_logs.rb +149 -0
  162. data/lib/active_record/querying.rb +16 -6
  163. data/lib/active_record/railtie.rb +136 -22
  164. data/lib/active_record/railties/controller_runtime.rb +1 -1
  165. data/lib/active_record/railties/databases.rake +78 -136
  166. data/lib/active_record/readonly_attributes.rb +11 -0
  167. data/lib/active_record/reflection.rb +73 -50
  168. data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
  169. data/lib/active_record/relation/batches.rb +6 -6
  170. data/lib/active_record/relation/calculations.rb +43 -38
  171. data/lib/active_record/relation/delegation.rb +7 -7
  172. data/lib/active_record/relation/finder_methods.rb +31 -35
  173. data/lib/active_record/relation/merger.rb +20 -13
  174. data/lib/active_record/relation/predicate_builder.rb +1 -6
  175. data/lib/active_record/relation/query_attribute.rb +5 -11
  176. data/lib/active_record/relation/query_methods.rb +276 -67
  177. data/lib/active_record/relation/record_fetch_warning.rb +7 -9
  178. data/lib/active_record/relation/spawn_methods.rb +2 -2
  179. data/lib/active_record/relation/where_clause.rb +10 -19
  180. data/lib/active_record/relation.rb +189 -88
  181. data/lib/active_record/result.rb +17 -7
  182. data/lib/active_record/runtime_registry.rb +9 -13
  183. data/lib/active_record/sanitization.rb +17 -12
  184. data/lib/active_record/schema.rb +38 -23
  185. data/lib/active_record/schema_dumper.rb +25 -19
  186. data/lib/active_record/schema_migration.rb +4 -4
  187. data/lib/active_record/scoping/default.rb +60 -13
  188. data/lib/active_record/scoping/named.rb +3 -11
  189. data/lib/active_record/scoping.rb +64 -34
  190. data/lib/active_record/serialization.rb +6 -1
  191. data/lib/active_record/signed_id.rb +3 -3
  192. data/lib/active_record/store.rb +1 -1
  193. data/lib/active_record/suppressor.rb +11 -15
  194. data/lib/active_record/tasks/database_tasks.rb +127 -60
  195. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  196. data/lib/active_record/tasks/postgresql_database_tasks.rb +19 -13
  197. data/lib/active_record/test_databases.rb +1 -1
  198. data/lib/active_record/test_fixtures.rb +9 -6
  199. data/lib/active_record/timestamp.rb +3 -4
  200. data/lib/active_record/transactions.rb +9 -14
  201. data/lib/active_record/translation.rb +3 -3
  202. data/lib/active_record/type/adapter_specific_registry.rb +32 -7
  203. data/lib/active_record/type/hash_lookup_type_map.rb +34 -1
  204. data/lib/active_record/type/internal/timezone.rb +2 -2
  205. data/lib/active_record/type/serialized.rb +1 -1
  206. data/lib/active_record/type/type_map.rb +17 -20
  207. data/lib/active_record/type.rb +1 -2
  208. data/lib/active_record/validations/associated.rb +4 -4
  209. data/lib/active_record/validations/presence.rb +2 -2
  210. data/lib/active_record/validations/uniqueness.rb +4 -4
  211. data/lib/active_record/version.rb +1 -1
  212. data/lib/active_record.rb +225 -27
  213. data/lib/arel/attributes/attribute.rb +0 -8
  214. data/lib/arel/crud.rb +28 -22
  215. data/lib/arel/delete_manager.rb +18 -4
  216. data/lib/arel/filter_predications.rb +9 -0
  217. data/lib/arel/insert_manager.rb +2 -3
  218. data/lib/arel/nodes/casted.rb +1 -1
  219. data/lib/arel/nodes/delete_statement.rb +12 -13
  220. data/lib/arel/nodes/filter.rb +10 -0
  221. data/lib/arel/nodes/function.rb +1 -0
  222. data/lib/arel/nodes/insert_statement.rb +2 -2
  223. data/lib/arel/nodes/select_core.rb +2 -2
  224. data/lib/arel/nodes/select_statement.rb +2 -2
  225. data/lib/arel/nodes/update_statement.rb +8 -3
  226. data/lib/arel/nodes.rb +1 -0
  227. data/lib/arel/predications.rb +11 -3
  228. data/lib/arel/select_manager.rb +10 -4
  229. data/lib/arel/table.rb +0 -1
  230. data/lib/arel/tree_manager.rb +0 -12
  231. data/lib/arel/update_manager.rb +18 -4
  232. data/lib/arel/visitors/dot.rb +80 -90
  233. data/lib/arel/visitors/mysql.rb +8 -2
  234. data/lib/arel/visitors/postgresql.rb +0 -10
  235. data/lib/arel/visitors/to_sql.rb +58 -2
  236. data/lib/arel.rb +2 -1
  237. data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
  238. data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +1 -1
  239. data/lib/rails/generators/active_record/model/templates/model.rb.tt +1 -1
  240. data/lib/rails/generators/active_record/model/templates/module.rb.tt +2 -2
  241. data/lib/rails/generators/active_record/multi_db/multi_db_generator.rb +16 -0
  242. data/lib/rails/generators/active_record/multi_db/templates/multi_db.rb.tt +44 -0
  243. metadata +53 -9
@@ -432,7 +432,7 @@ module ActiveRecord
432
432
  define_model_callbacks :save, :create, :update, :destroy
433
433
  end
434
434
 
435
- def destroy #:nodoc:
435
+ def destroy # :nodoc:
436
436
  @_destroy_callback_already_called ||= false
437
437
  return if @_destroy_callback_already_called
438
438
  @_destroy_callback_already_called = true
@@ -444,7 +444,7 @@ module ActiveRecord
444
444
  @_destroy_callback_already_called = false
445
445
  end
446
446
 
447
- def touch(*, **) #:nodoc:
447
+ def touch(*, **) # :nodoc:
448
448
  _run_touch_callbacks { super }
449
449
  end
450
450
 
@@ -47,22 +47,18 @@ module ActiveRecord
47
47
 
48
48
  if YAML.respond_to?(:unsafe_load)
49
49
  def yaml_load(payload)
50
- if ActiveRecord::Base.use_yaml_unsafe_load
50
+ if ActiveRecord.use_yaml_unsafe_load
51
51
  YAML.unsafe_load(payload)
52
- elsif YAML.method(:safe_load).parameters.include?([:key, :permitted_classes])
53
- YAML.safe_load(payload, permitted_classes: ActiveRecord::Base.yaml_column_permitted_classes, aliases: true)
54
52
  else
55
- YAML.safe_load(payload, ActiveRecord::Base.yaml_column_permitted_classes, [], true)
53
+ YAML.safe_load(payload, permitted_classes: ActiveRecord.yaml_column_permitted_classes, aliases: true)
56
54
  end
57
55
  end
58
56
  else
59
57
  def yaml_load(payload)
60
- if ActiveRecord::Base.use_yaml_unsafe_load
58
+ if ActiveRecord.use_yaml_unsafe_load
61
59
  YAML.load(payload)
62
- elsif YAML.method(:safe_load).parameters.include?([:key, :permitted_classes])
63
- YAML.safe_load(payload, permitted_classes: ActiveRecord::Base.yaml_column_permitted_classes, aliases: true)
64
60
  else
65
- YAML.safe_load(payload, ActiveRecord::Base.yaml_column_permitted_classes, [], true)
61
+ YAML.safe_load(payload, permitted_classes: ActiveRecord.yaml_column_permitted_classes, aliases: true)
66
62
  end
67
63
  end
68
64
  end
@@ -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