switchman 3.3.1 → 4.1.0

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +15 -14
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
  4. data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
  5. data/lib/switchman/active_record/abstract_adapter.rb +5 -3
  6. data/lib/switchman/active_record/associations.rb +91 -51
  7. data/lib/switchman/active_record/attribute_methods.rb +88 -43
  8. data/lib/switchman/active_record/base.rb +113 -40
  9. data/lib/switchman/active_record/calculations.rb +98 -51
  10. data/lib/switchman/active_record/connection_handler.rb +18 -0
  11. data/lib/switchman/active_record/connection_pool.rb +56 -6
  12. data/lib/switchman/active_record/database_configurations.rb +37 -15
  13. data/lib/switchman/active_record/finder_methods.rb +47 -17
  14. data/lib/switchman/active_record/log_subscriber.rb +11 -5
  15. data/lib/switchman/active_record/migration.rb +51 -3
  16. data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
  17. data/lib/switchman/active_record/persistence.rb +30 -0
  18. data/lib/switchman/active_record/postgresql_adapter.rb +37 -22
  19. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  20. data/lib/switchman/active_record/query_cache.rb +57 -20
  21. data/lib/switchman/active_record/query_methods.rb +148 -44
  22. data/lib/switchman/active_record/reflection.rb +9 -2
  23. data/lib/switchman/active_record/relation.rb +79 -15
  24. data/lib/switchman/active_record/spawn_methods.rb +3 -7
  25. data/lib/switchman/active_record/statement_cache.rb +2 -2
  26. data/lib/switchman/active_record/table_definition.rb +1 -1
  27. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  28. data/lib/switchman/active_record/test_fixtures.rb +75 -25
  29. data/lib/switchman/active_support/cache.rb +9 -4
  30. data/lib/switchman/arel.rb +34 -18
  31. data/lib/switchman/call_super.rb +2 -8
  32. data/lib/switchman/database_server.rb +72 -34
  33. data/lib/switchman/default_shard.rb +14 -3
  34. data/lib/switchman/engine.rb +38 -22
  35. data/lib/switchman/environment.rb +2 -2
  36. data/lib/switchman/errors.rb +13 -0
  37. data/lib/switchman/guard_rail/relation.rb +1 -2
  38. data/lib/switchman/parallel.rb +6 -6
  39. data/lib/switchman/r_spec_helper.rb +12 -11
  40. data/lib/switchman/shard.rb +185 -71
  41. data/lib/switchman/sharded_instrumenter.rb +3 -3
  42. data/lib/switchman/shared_schema_cache.rb +11 -0
  43. data/lib/switchman/standard_error.rb +4 -0
  44. data/lib/switchman/test_helper.rb +3 -3
  45. data/lib/switchman/version.rb +1 -1
  46. data/lib/switchman.rb +27 -15
  47. data/lib/tasks/switchman.rake +96 -60
  48. metadata +50 -46
@@ -4,39 +4,89 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module TestFixtures
6
6
  FORBIDDEN_DB_ENVS = %i[development production].freeze
7
- def setup_fixtures(config = ::ActiveRecord::Base)
8
- super
9
7
 
10
- return unless run_in_transaction?
8
+ if ::Rails.version < "7.2"
9
+ def setup_fixtures(config = ::ActiveRecord::Base)
10
+ super
11
+ return unless run_in_transaction?
11
12
 
12
- # Replace the one that activerecord natively uses with a switchman-optimized one
13
- ::ActiveSupport::Notifications.unsubscribe(@connection_subscriber)
14
- # Code adapted from the code in rails proper
15
- @connection_subscriber = ::ActiveSupport::Notifications.subscribe('!connection.active_record') do |_, _, _, _, payload|
16
- spec_name = payload[:spec_name] if payload.key?(:spec_name)
17
- shard = payload[:shard] if payload.key?(:shard)
18
- setup_shared_connection_pool
13
+ # Replace the one that activerecord natively uses with a switchman-optimized one
14
+ ::ActiveSupport::Notifications.unsubscribe(@connection_subscriber)
15
+ # Code adapted from the code in rails proper
16
+ @connection_subscriber =
17
+ ::ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload|
18
+ spec_name = if ::Rails.version < "7.1"
19
+ payload[:spec_name] if payload.key?(:spec_name)
20
+ elsif payload.key?(:connection_name)
21
+ payload[:connection_name]
22
+ end
23
+ shard = payload[:shard] if payload.key?(:shard)
19
24
 
20
- if spec_name && !FORBIDDEN_DB_ENVS.include?(shard)
21
- begin
22
- connection = ::ActiveRecord::Base.connection_handler.retrieve_connection(spec_name, shard: shard)
23
- rescue ::ActiveRecord::ConnectionNotEstablished, ::ActiveRecord::NoDatabaseError
24
- connection = nil
25
- end
25
+ if spec_name && !FORBIDDEN_DB_ENVS.include?(shard)
26
+ begin
27
+ connection = ::ActiveRecord::Base.connection_handler.retrieve_connection(spec_name, shard: shard)
28
+ connection.connect! if ::Rails.version >= "7.1" # eagerly validate the connection
29
+ rescue ::ActiveRecord::ConnectionNotEstablished
30
+ connection = nil
31
+ end
26
32
 
27
- if connection && !@fixture_connections.include?(connection)
28
- connection.begin_transaction joinable: false, _lazy: false
29
- connection.pool.lock_thread = true if lock_threads
30
- @fixture_connections << connection
33
+ if connection
34
+ setup_shared_connection_pool
35
+ unless @fixture_connections.include?(connection)
36
+ connection.begin_transaction joinable: false, _lazy: false
37
+ connection.pool.lock_thread = true if lock_threads
38
+ @fixture_connections << connection
39
+ end
40
+ end
41
+ end
31
42
  end
32
- end
33
43
  end
34
- end
35
44
 
36
- def enlist_fixture_connections
37
- setup_shared_connection_pool
45
+ def enlist_fixture_connections
46
+ setup_shared_connection_pool
38
47
 
39
- ::ActiveRecord::Base.connection_handler.connection_pool_list.reject { |cp| FORBIDDEN_DB_ENVS.include?(cp.db_config.env_name.to_sym) }.map(&:connection)
48
+ ::ActiveRecord::Base.connection_handler.connection_pool_list(:primary).reject do |cp|
49
+ FORBIDDEN_DB_ENVS.include?(cp.db_config.env_name.to_sym)
50
+ end.map(&:connection)
51
+ end
52
+ else
53
+ def setup_transactional_fixtures
54
+ setup_shared_connection_pool
55
+
56
+ # Begin transactions for connections already established
57
+ # INST: :writing -> :primary
58
+ @fixture_connection_pools = ::ActiveRecord::Base.connection_handler.connection_pool_list(:primary)
59
+ # INST: filter by FORBIDDEN_DB_ENVS
60
+ @fixture_connection_pools = @fixture_connection_pools.reject do |cp|
61
+ FORBIDDEN_DB_ENVS.include?(cp.db_config.env_name.to_sym)
62
+ end
63
+
64
+ @fixture_connection_pools.each do |pool|
65
+ pool.pin_connection!(lock_threads)
66
+ pool.lease_connection
67
+ end
68
+
69
+ # When connections are established in the future, begin a transaction too
70
+ @connection_subscriber = ::ActiveSupport::Notifications
71
+ .subscribe("!connection.active_record") do |_, _, _, _, payload|
72
+ connection_name = payload[:connection_name] if payload.key?(:connection_name)
73
+ shard = payload[:shard] if payload.key?(:shard)
74
+
75
+ # INST: filter by FORBIDDEN_DB_ENVS
76
+ if connection_name && !FORBIDDEN_DB_ENVS.include?(shard)
77
+ pool = ::ActiveRecord::Base.connection_handler.retrieve_connection_pool(connection_name, shard: shard)
78
+ if pool
79
+ setup_shared_connection_pool
80
+
81
+ unless @fixture_connection_pools.include?(pool)
82
+ pool.pin_connection!(lock_threads)
83
+ pool.lease_connection
84
+ @fixture_connection_pools << pool
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
40
90
  end
41
91
  end
42
92
  end
@@ -22,9 +22,14 @@ module Switchman
22
22
 
23
23
  def lookup_store(*store_options)
24
24
  store = super
25
- # can't use defined?, because it's a _ruby_ autoloaded constant,
26
- # so just checking that will cause it to get required
27
- ::ActiveSupport::Cache::RedisCacheStore.prepend(RedisCacheStore) if store.instance_of?(ActiveSupport::Cache::RedisCacheStore) && !::ActiveSupport::Cache::RedisCacheStore.ancestors.include?(RedisCacheStore)
25
+ # must use the string name, otherwise it will try to auto-load the constant
26
+ # and we don't want to require redis in this file (since it's not a hard dependency)
27
+ # rubocop:disable Style/ClassEqualityComparison
28
+ if store.class.name == "ActiveSupport::Cache::RedisCacheStore" &&
29
+ !(::ActiveSupport::Cache::RedisCacheStore <= RedisCacheStore)
30
+ ::ActiveSupport::Cache::RedisCacheStore.prepend(RedisCacheStore)
31
+ end
32
+ # rubocop:enable Style/ClassEqualityComparison
28
33
  store.options[:namespace] ||= -> { Shard.current.default? ? nil : "shard_#{Shard.current.id}" }
29
34
  store
30
35
  end
@@ -33,7 +38,7 @@ module Switchman
33
38
  module RedisCacheStore
34
39
  def clear(namespace: nil, **)
35
40
  # RedisCacheStore tries to be smart and only clear the cache under your namespace, if you have one set
36
- # unfortunately, it uses the keys command, which is extraordinarily inefficient in a large redis instance
41
+ # unfortunately, it doesn't work using redis clustering because of the way redis keys are distributed
37
42
  # fortunately, we can assume we control the entire instance, because we set up the namespacing, so just
38
43
  # always unset it temporarily for clear calls
39
44
  namespace = nil # rubocop:disable Lint/ShadowedArgument
@@ -13,38 +13,54 @@ module Switchman
13
13
  # rubocop:disable Naming/MethodName
14
14
  # rubocop:disable Naming/MethodParameterName
15
15
 
16
+ def visit_Arel_Nodes_Cte(o, collector)
17
+ collector << quote_local_table_name(o.name)
18
+ collector << " AS "
19
+
20
+ case o.materialized
21
+ when true
22
+ collector << "MATERIALIZED "
23
+ when false
24
+ collector << "NOT MATERIALIZED "
25
+ end
26
+
27
+ visit o.relation, collector
28
+ end
29
+
16
30
  def visit_Arel_Nodes_TableAlias(o, collector)
17
31
  collector = visit o.relation, collector
18
- collector << ' '
32
+ collector << " "
19
33
  collector << quote_local_table_name(o.name)
20
34
  end
21
35
 
22
36
  def visit_Arel_Attributes_Attribute(o, collector)
23
37
  join_name = o.relation.table_alias || o.relation.name
24
- collector << quote_local_table_name(join_name) << '.' << quote_column_name(o.name)
38
+ collector << quote_local_table_name(join_name) << "." << quote_column_name(o.name)
25
39
  end
26
40
 
27
- def visit_Arel_Nodes_HomogeneousIn(o, collector)
28
- collector.preparable = false
41
+ if ::Rails.version < "7.1"
42
+ def visit_Arel_Nodes_HomogeneousIn(o, collector)
43
+ collector.preparable = false
29
44
 
30
- collector << quote_local_table_name(o.table_name) << '.' << quote_column_name(o.column_name)
45
+ collector << quote_local_table_name(o.table_name) << "." << quote_column_name(o.column_name)
31
46
 
32
- collector << if o.type == :in
33
- ' IN ('
34
- else
35
- ' NOT IN ('
36
- end
47
+ collector << if o.type == :in
48
+ " IN ("
49
+ else
50
+ " NOT IN ("
51
+ end
37
52
 
38
- values = o.casted_values
53
+ values = o.casted_values
39
54
 
40
- if values.empty?
41
- collector << @connection.quote(nil)
42
- else
43
- collector.add_binds(values, o.proc_for_binds, &bind_block)
44
- end
55
+ if values.empty?
56
+ collector << @connection.quote(nil)
57
+ else
58
+ collector.add_binds(values, o.proc_for_binds, &bind_block)
59
+ end
45
60
 
46
- collector << ')'
47
- collector
61
+ collector << ")"
62
+ collector
63
+ end
48
64
  end
49
65
 
50
66
  # rubocop:enable Naming/MethodName
@@ -12,14 +12,8 @@ module Switchman
12
12
  method.super_method
13
13
  end
14
14
 
15
- if RUBY_VERSION <= '2.8'
16
- def call_super(method, above_module, *args, &block)
17
- super_method_above(method, above_module).call(*args, &block)
18
- end
19
- else
20
- def call_super(method, above_module, *args, **kwargs, &block)
21
- super_method_above(method, above_module).call(*args, **kwargs, &block)
22
- end
15
+ def call_super(method, above_module, ...)
16
+ super_method_above(method, above_module).call(...)
23
17
  end
24
18
  end
25
19
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'securerandom'
3
+ require "securerandom"
4
4
 
5
5
  module Switchman
6
6
  class DatabaseServer
@@ -10,19 +10,23 @@ module Switchman
10
10
  attr_accessor :creating_new_shard
11
11
  attr_reader :all_roles
12
12
 
13
+ include Enumerable
14
+
15
+ delegate :each, to: :all
16
+
13
17
  def all
14
18
  database_servers.values
15
19
  end
16
20
 
17
21
  def find(id_or_all)
18
22
  return all if id_or_all == :all
19
- return id_or_all.map { |id| database_servers[id || ::Rails.env] }.compact.uniq if id_or_all.is_a?(Array)
23
+ return id_or_all.filter_map { |id| database_servers[id || ::Rails.env] }.uniq if id_or_all.is_a?(Array)
20
24
 
21
25
  database_servers[id_or_all || ::Rails.env]
22
26
  end
23
27
 
24
28
  def create(settings = {})
25
- raise 'database servers should be set up in database.yml' unless ::Rails.env.test?
29
+ raise "database servers should be set up in database.yml" unless ::Rails.env.test?
26
30
 
27
31
  id = settings[:id]
28
32
  unless id
@@ -50,6 +54,10 @@ module Switchman
50
54
  all.each { |db| db.guard! if db.config[:prefer_secondary] }
51
55
  end
52
56
 
57
+ def regions
58
+ @regions ||= all.filter_map(&:region).uniq.sort
59
+ end
60
+
53
61
  private
54
62
 
55
63
  def reference_role(role)
@@ -64,8 +72,8 @@ module Switchman
64
72
  @database_servers = {}.with_indifferent_access
65
73
  roles = []
66
74
  ::ActiveRecord::Base.configurations.configurations.each do |config|
67
- if config.name.include?('/')
68
- name, role = config.name.split('/')
75
+ if config.name.include?("/")
76
+ name, role = config.name.split("/")
69
77
  else
70
78
  name, role = config.env_name, config.name
71
79
  end
@@ -81,6 +89,8 @@ module Switchman
81
89
  # Do this after so that all database servers for all roles are established and we won't prematurely
82
90
  # configure a connection for the wrong role
83
91
  @all_roles = roles.uniq
92
+ return @database_servers if @database_servers.empty?
93
+
84
94
  Shard.send(:configure_connects_to)
85
95
  end
86
96
  @database_servers
@@ -110,8 +120,9 @@ module Switchman
110
120
  self.class.send(:database_servers).delete(id) if id
111
121
  Shard.sharded_models.each do |klass|
112
122
  self.class.all_roles.each do |role|
113
- klass.connection_handler.remove_connection_pool(klass.connection_specification_name, role: role,
114
- shard: id.to_sym)
123
+ klass.connection_handler.remove_connection_pool(klass.connection_specification_name,
124
+ role:,
125
+ shard: id.to_sym)
115
126
  end
116
127
  end
117
128
  end
@@ -137,16 +148,41 @@ module Switchman
137
148
  end
138
149
  end
139
150
 
151
+ def region
152
+ config[:region]
153
+ end
154
+
155
+ # @param region [String, Array<String>] the region(s) to check against
156
+ # @return true if the database server doesn't have a region, or it
157
+ # matches the specified region
158
+ def in_region?(region)
159
+ !self.region || (region.is_a?(Array) ? region.include?(self.region) : self.region == region)
160
+ end
161
+
162
+ # @return true if the database server doesn't have a region, Switchman is
163
+ # not configured with a region, or the database server's region matches
164
+ # Switchman's current region
165
+ def in_current_region?
166
+ unless instance_variable_defined?(:@in_current_region)
167
+ @in_current_region = !region ||
168
+ !Switchman.region ||
169
+ region == Switchman.region
170
+ end
171
+ @in_current_region
172
+ end
173
+
140
174
  # locks this db to a specific environment, except for
141
175
  # when doing writes (then it falls back to the current
142
176
  # value of GuardRail.environment)
143
177
  def guard!(environment = :secondary)
144
178
  DatabaseServer.send(:reference_role, environment)
145
- ::ActiveRecord::Base.connected_to_stack << { shard_roles: { id.to_sym => environment }, klasses: [::ActiveRecord::Base] }
179
+ ::ActiveRecord::Base.connected_to_stack << { shard_roles: { id.to_sym => environment },
180
+ klasses: [::ActiveRecord::Base] }
146
181
  end
147
182
 
148
183
  def unguard!
149
- ::ActiveRecord::Base.connected_to_stack << { shard_roles: { id.to_sym => :_switchman_inherit }, klasses: [::ActiveRecord::Base] }
184
+ ::ActiveRecord::Base.connected_to_stack << { shard_roles: { id.to_sym => :_switchman_inherit },
185
+ klasses: [::ActiveRecord::Base] }
150
186
  end
151
187
 
152
188
  def unguard
@@ -162,7 +198,7 @@ module Switchman
162
198
 
163
199
  def shards
164
200
  if id == ::Rails.env
165
- Shard.where('database_server_id IS NULL OR database_server_id=?', id)
201
+ Shard.where("database_server_id IS NULL OR database_server_id=?", id)
166
202
  else
167
203
  Shard.where(database_server_id: id)
168
204
  end
@@ -182,12 +218,12 @@ module Switchman
182
218
  if config_create_statement
183
219
  create_commands = Array(config_create_statement).dup
184
220
  create_statement = lambda {
185
- create_commands.map { |statement| format(statement, name: name, password: password) }
221
+ create_commands.map { |statement| format(statement, name:, password:) }
186
222
  }
187
223
  end
188
224
 
189
225
  id ||= begin
190
- id_seq = Shard.connection.quote(Shard.connection.quote_table_name('switchman_shards_id_seq'))
226
+ id_seq = Shard.connection.quote(Shard.connection.quote_table_name("switchman_shards_id_seq"))
191
227
  next_id = Shard.connection.select_value("SELECT nextval(#{id_seq})")
192
228
  next_id.to_i
193
229
  end
@@ -200,33 +236,36 @@ module Switchman
200
236
  self.class.creating_new_shard = true
201
237
  DatabaseServer.send(:reference_role, :deploy)
202
238
  ::ActiveRecord::Base.connected_to(shard: self.id.to_sym, role: :deploy) do
203
- shard = Shard.create!(id: id,
204
- name: name,
239
+ shard = Shard.create!(id:,
240
+ name:,
205
241
  database_server_id: self.id)
206
242
  if create_statement
207
- if ::ActiveRecord::Base.connection.select_value("SELECT 1 FROM pg_namespace WHERE nspname=#{::ActiveRecord::Base.connection.quote(name)}")
243
+ if ::ActiveRecord::Base.connection.select_value(
244
+ "SELECT 1 FROM pg_namespace WHERE nspname=#{::ActiveRecord::Base.connection.quote(name)}"
245
+ )
208
246
  schema_already_existed = true
209
- raise 'This schema already exists; cannot overwrite'
247
+ raise "This schema already exists; cannot overwrite"
210
248
  end
211
249
  Array(create_statement.call).each do |stmt|
212
250
  ::ActiveRecord::Base.connection.execute(stmt)
213
251
  end
214
252
  end
215
- if config[:adapter] == 'postgresql'
216
- old_proc = ::ActiveRecord::Base.connection.raw_connection.set_notice_processor do
217
- end
253
+ if config[:adapter] == "postgresql"
254
+ old_proc = ::ActiveRecord::Base.connection.raw_connection.set_notice_processor {}
218
255
  end
219
256
  old_verbose = ::ActiveRecord::Migration.verbose
220
257
  ::ActiveRecord::Migration.verbose = false
221
258
 
222
259
  unless schema == false
223
260
  shard.activate do
224
- reset_column_information
225
-
226
261
  ::ActiveRecord::Base.connection.transaction(requires_new: true) do
227
- ::ActiveRecord::Base.connection.migration_context.migrate
262
+ if ::Rails.version < "7.1"
263
+ ::ActiveRecord::Base.connection.migration_context.migrate
264
+ else
265
+ ::ActiveRecord::MigrationContext.new(::ActiveRecord::Migrator.migrations_paths).migrate
266
+ end
228
267
  end
229
- reset_column_information
268
+
230
269
  ::ActiveRecord::Base.descendants.reject do |m|
231
270
  m <= UnshardedRecord || !m.table_exists?
232
271
  end.each(&:define_attribute_methods)
@@ -240,7 +279,6 @@ module Switchman
240
279
  rescue
241
280
  shard&.destroy
242
281
  shard&.drop_database rescue nil unless schema_already_existed
243
- reset_column_information unless schema == false rescue nil
244
282
  raise
245
283
  ensure
246
284
  self.class.creating_new_shard = false
@@ -265,18 +303,18 @@ module Switchman
265
303
  end
266
304
 
267
305
  def primary_shard
268
- unless instance_variable_defined?(:@primary_shard)
269
- # if sharding isn't fully set up yet, we may not be able to query the shards table
270
- @primary_shard = Shard.default if Shard.default.database_server == self
271
- @primary_shard ||= shards.where(name: nil).first
272
- end
273
- @primary_shard
274
- end
306
+ return nil unless primary_shard_id
275
307
 
276
- private
308
+ Shard.lookup(primary_shard_id)
309
+ end
277
310
 
278
- def reset_column_information
279
- ::ActiveRecord::Base.descendants.reject { |m| m <= UnshardedRecord }.each(&:reset_column_information)
311
+ def primary_shard_id
312
+ unless instance_variable_defined?(:@primary_shard_id)
313
+ # if sharding isn't fully set up yet, we may not be able to query the shards table
314
+ @primary_shard_id = Shard.default.id if Shard.default.database_server == self
315
+ @primary_shard_id ||= shards.where(name: nil).first&.id
316
+ end
317
+ @primary_shard_id
280
318
  end
281
319
  end
282
320
  end
@@ -3,9 +3,10 @@
3
3
  module Switchman
4
4
  class DefaultShard
5
5
  def id
6
- 'default'
6
+ "default"
7
7
  end
8
- alias cache_key id
8
+ alias_method :cache_key, :id
9
+
9
10
  def activate(*_classes)
10
11
  yield
11
12
  end
@@ -57,8 +58,18 @@ module Switchman
57
58
  self
58
59
  end
59
60
 
61
+ def region; end
62
+
63
+ def in_region?(_region)
64
+ true
65
+ end
66
+
67
+ def in_current_region?
68
+ true
69
+ end
70
+
60
71
  def _dump(_depth)
61
- ''
72
+ ""
62
73
  end
63
74
 
64
75
  def self._load(_str)
@@ -5,24 +5,17 @@ module Switchman
5
5
  isolate_namespace Switchman
6
6
 
7
7
  # enable Rails 6.1 style connection handling
8
- config.active_record.legacy_connection_handling = false
8
+ config.active_record.legacy_connection_handling = false if ::Rails.version < "7.1"
9
9
  config.active_record.writing_role = :primary
10
10
 
11
11
  ::GuardRail.singleton_class.prepend(GuardRail::ClassMethods)
12
12
 
13
- # after :initialize_dependency_mechanism to ensure autoloading is configured for any downstream initializers that care
14
- # In rails 7.0 we should be able to just use an explicit after on configuring the once autoloaders and not need to go monkey around with initializer order
15
- if ::Rails.version < '7.0'
16
- initialize_dependency_mechanism = ::Rails::Application::Bootstrap.initializers.find { |i| i.name == :initialize_dependency_mechanism }
17
- initialize_dependency_mechanism.instance_variable_get(:@options)[:after] = :set_autoload_paths
18
- end
19
-
20
- initializer 'switchman.active_record_patch',
21
- before: 'active_record.initialize_database',
22
- after: (::Rails.version < '7.0' ? :initialize_dependency_mechanism : :setup_once_autoloader) do
13
+ initializer "switchman.active_record_patch",
14
+ before: "active_record.initialize_database",
15
+ after: :setup_once_autoloader do
23
16
  ::ActiveSupport.on_load(:active_record) do
24
17
  # Switchman requires postgres, so just always load the pg adapter
25
- require 'active_record/connection_adapters/postgresql_adapter'
18
+ require "active_record/connection_adapters/postgresql_adapter"
26
19
 
27
20
  self.default_shard = ::Rails.env.to_sym
28
21
  self.default_role = :primary
@@ -50,14 +43,29 @@ module Switchman
50
43
  ::ActiveRecord::Associations::CollectionProxy.include(ActiveRecord::Associations::CollectionProxy)
51
44
 
52
45
  ::ActiveRecord::Associations::Preloader::Association.prepend(ActiveRecord::Associations::Preloader::Association)
53
- ::ActiveRecord::Associations::Preloader::Association::LoaderRecords.prepend(ActiveRecord::Associations::Preloader::Association::LoaderRecords) unless ::Rails.version < '7.0'
46
+ ::ActiveRecord::Associations::Preloader::Association::LoaderRecords.prepend(
47
+ ActiveRecord::Associations::Preloader::Association::LoaderRecords
48
+ )
54
49
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::AbstractAdapter)
50
+ unless ::Rails.version < "7.1"
51
+ ::ActiveRecord::ConnectionAdapters::ConnectionHandler.prepend(ActiveRecord::ConnectionHandler)
52
+ end
55
53
  ::ActiveRecord::ConnectionAdapters::ConnectionPool.prepend(ActiveRecord::ConnectionPool)
56
54
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::QueryCache)
57
55
  ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(ActiveRecord::PostgreSQLAdapter)
56
+ # https://github.com/rails/rails/commit/0016280f4fde55d96738887093dc333aae0d107b
57
+ if ::Rails.version < "7.2"
58
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(ActiveRecord::PostgreSQLAdapter::ClassMethods)
59
+ else
60
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.singleton_class.prepend(
61
+ ActiveRecord::PostgreSQLAdapter::ClassMethods
62
+ )
63
+ end
58
64
 
59
65
  ::ActiveRecord::DatabaseConfigurations.prepend(ActiveRecord::DatabaseConfigurations)
60
- ::ActiveRecord::DatabaseConfigurations::DatabaseConfig.prepend(ActiveRecord::DatabaseConfigurations::DatabaseConfig)
66
+ ::ActiveRecord::DatabaseConfigurations::DatabaseConfig.prepend(
67
+ ActiveRecord::DatabaseConfigurations::DatabaseConfig
68
+ )
61
69
 
62
70
  ::ActiveRecord::LogSubscriber.prepend(ActiveRecord::LogSubscriber)
63
71
  ::ActiveRecord::Migration.prepend(ActiveRecord::Migration)
@@ -65,6 +73,11 @@ module Switchman
65
73
  ::ActiveRecord::MigrationContext.prepend(ActiveRecord::MigrationContext)
66
74
  ::ActiveRecord::Migrator.prepend(ActiveRecord::Migrator)
67
75
 
76
+ if ::Rails.version > "7.1.3"
77
+ ::ActiveRecord::PendingMigrationConnection.singleton_class
78
+ .include(ActiveRecord::PendingMigrationConnection::ClassMethods)
79
+ end
80
+
68
81
  ::ActiveRecord::Reflection::AbstractReflection.include(ActiveRecord::Reflection::AbstractReflection)
69
82
  ::ActiveRecord::Reflection::AssociationReflection.prepend(ActiveRecord::Reflection::AssociationScopeCache)
70
83
  ::ActiveRecord::Reflection::ThroughReflection.prepend(ActiveRecord::Reflection::AssociationScopeCache)
@@ -74,11 +87,13 @@ module Switchman
74
87
  ::ActiveRecord::Relation.include(ActiveRecord::QueryMethods)
75
88
  ::ActiveRecord::Relation.prepend(GuardRail::Relation)
76
89
  ::ActiveRecord::Relation.prepend(ActiveRecord::Relation)
90
+ ::ActiveRecord::Relation.prepend(ActiveRecord::Relation::InsertUpsertAll) if ::Rails.version >= "7.2"
77
91
  ::ActiveRecord::Relation.include(ActiveRecord::SpawnMethods)
78
92
  ::ActiveRecord::Relation.include(CallSuper)
79
93
 
80
- ::ActiveRecord::PredicateBuilder::AssociationQueryValue.prepend(ActiveRecord::PredicateBuilder::AssociationQueryValue)
81
- ::ActiveRecord::PredicateBuilder::PolymorphicArrayValue.prepend(ActiveRecord::PredicateBuilder::AssociationQueryValue)
94
+ ::ActiveRecord::PredicateBuilder::PolymorphicArrayValue.prepend(
95
+ ActiveRecord::PredicateBuilder::PolymorphicArrayValue
96
+ )
82
97
 
83
98
  ::ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(ActiveRecord::Tasks::DatabaseTasks)
84
99
 
@@ -96,17 +111,18 @@ module Switchman
96
111
 
97
112
  ::ActiveRecord::ConnectionAdapters::TableDefinition.prepend(ActiveRecord::TableDefinition)
98
113
  end
99
- # Ensure that ActiveRecord::Base is always loaded before any app-level initializers can go try to load Switchman::Shard or we get a loop
114
+ # Ensure that ActiveRecord::Base is always loaded before any app-level
115
+ # initializers can go try to load Switchman::Shard or we get a loop
100
116
  ::ActiveRecord::Base
101
117
  end
102
118
 
103
- initializer 'switchman.error_patch', after: 'active_record.initialize_database' do
119
+ initializer "switchman.error_patch", after: "active_record.initialize_database" do
104
120
  ::ActiveSupport.on_load(:active_record) do
105
121
  ::StandardError.include(StandardError)
106
122
  end
107
123
  end
108
124
 
109
- initializer 'switchman.initialize_cache', before: :initialize_cache, after: 'active_record.initialize_database' do
125
+ initializer "switchman.initialize_cache", before: :initialize_cache, after: "active_record.initialize_database" do
110
126
  ::ActiveSupport::Cache.singleton_class.prepend(ActiveSupport::Cache::ClassMethods)
111
127
 
112
128
  # if we haven't already setup our cache map out-of-band, set it up from
@@ -130,11 +146,11 @@ module Switchman
130
146
  Switchman.config[:cache_map][::Rails.env] = value
131
147
  end
132
148
 
133
- middlewares = Switchman.config[:cache_map].values.map do |store|
149
+ middlewares = Switchman.config[:cache_map].values.filter_map do |store|
134
150
  store.middleware if store.respond_to?(:middleware)
135
- end.compact.uniq
151
+ end.uniq
136
152
  middlewares.each do |middleware|
137
- config.middleware.insert_before('Rack::Runtime', middleware)
153
+ config.middleware.insert_before("Rack::Runtime", middleware)
138
154
  end
139
155
 
140
156
  # prevent :initialize_cache from trying to (or needing to) set
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'etc'
3
+ require "etc"
4
4
 
5
5
  module Switchman
6
6
  class Environment
7
- def self.cpu_count(nproc_bin = 'nproc')
7
+ def self.cpu_count(nproc_bin = "nproc")
8
8
  return Etc.nprocessors if Etc.respond_to?(:nprocessors)
9
9
 
10
10
  `#{nproc_bin}`.to_i
@@ -2,8 +2,21 @@
2
2
 
3
3
  module Switchman
4
4
  module Errors
5
+ class ManuallyCreatedShadowRecordError < RuntimeError
6
+ DEFAULT_MSG = "It looks like you're trying to manually create a shadow record. " \
7
+ "Please use Switchman::ActiveRecord::Base#save_shadow_record instead."
8
+
9
+ def initialize(msg = DEFAULT_MSG)
10
+ super
11
+ end
12
+ end
13
+
5
14
  class NonExistentShardError < RuntimeError; end
6
15
 
7
16
  class ParallelShardExecError < RuntimeError; end
17
+
18
+ class ShadowRecordError < RuntimeError; end
19
+
20
+ class UnshardedTableError < RuntimeError; end
8
21
  end
9
22
  end