switchman 3.5.9 → 3.5.16

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64f3ba29604ff0a4f1db0b5c49000a636865f38c45ad6cb15eae252d98623d49
4
- data.tar.gz: 68a22ead418800884673abd7a08ab84a38f548b065930e28258a25947ac4ab8e
3
+ metadata.gz: a27299a15c04348fc6a632a8c3c53b8c4fa541140b8befd07f63bc3544f01f50
4
+ data.tar.gz: d9a43fc82c874a87026c5d0b9467a45ba71df19e0c94166f22cc1aabe11b8e5f
5
5
  SHA512:
6
- metadata.gz: 49bf3b77179a2d5a5788cfaec67aababa9bece27589bddadc387f2eafc2651b1cd6ac6397431d5a955fad05747acd8af2fe6e1c59597a447bf91209508771138
7
- data.tar.gz: ed4ae505a24d605802c67793a69d8462905ed16f638490e8fa0bdca1bb8e55f2b49f154e9c7ebd85ac6d2ba92321e7de962a559a5489e7821cc094da944d207f
6
+ metadata.gz: 2b47b69ec2830ed31204d0fe122d555b9f6623683469d6b35329cc1bfe1a18b6e5ec769985d792f64c72a696a4a16f4119407a9323b2560f4be575f52d5c09b9
7
+ data.tar.gz: 19bd4da4fa0ba4e4ec03c57858481f6dddd799b98bcd9e67e3870bbfb60c7cb69d44ba74781788c084069dc5bc17632f74b3c793d1964a8ef76122f2f78c144e
@@ -27,8 +27,10 @@ module Switchman
27
27
  quote_table_name(name)
28
28
  end
29
29
 
30
- def schema_migration
31
- ::ActiveRecord::SchemaMigration
30
+ if ::Rails.version < "7.1"
31
+ def schema_migration
32
+ ::ActiveRecord::SchemaMigration
33
+ end
32
34
  end
33
35
 
34
36
  protected
@@ -298,11 +298,56 @@ module Switchman
298
298
  record.has_attribute?(reflection.foreign_key) && record.send(reflection.foreign_key) != key
299
299
  end
300
300
 
301
- def save_belongs_to_association(reflection)
302
- # this seems counter-intuitive, but the autosave code will assign to attribute bypassing switchman,
303
- # after reading the id attribute _without_ bypassing switchman. So we need Shard.current for the
304
- # category of the associated record to match Shard.current for the category of self
305
- shard.activate(connection_class_for_self_for_reflection(reflection)) { super }
301
+ if ::Rails.version < "7.1"
302
+ def save_belongs_to_association(reflection)
303
+ # this seems counter-intuitive, but the autosave code will assign to attribute bypassing switchman,
304
+ # after reading the id attribute _without_ bypassing switchman. So we need Shard.current for the
305
+ # category of the associated record to match Shard.current for the category of self
306
+ shard.activate(connection_class_for_self_for_reflection(reflection)) { super }
307
+ end
308
+ else
309
+ def save_belongs_to_association(reflection)
310
+ association = association_instance_get(reflection.name)
311
+ return unless association&.loaded? && !association.stale_target?
312
+
313
+ record = association.load_target
314
+ return unless record && !record.destroyed?
315
+
316
+ autosave = reflection.options[:autosave]
317
+
318
+ if autosave && record.marked_for_destruction?
319
+ foreign_key = Array(reflection.foreign_key)
320
+ foreign_key.each { |key| self[key] = nil }
321
+ record.destroy
322
+ elsif autosave != false
323
+ if record.new_record? || (autosave && record.changed_for_autosave?)
324
+ saved = record.save(validate: !autosave)
325
+ end
326
+
327
+ if association.updated?
328
+ primary_key = Array(compute_primary_key(reflection, record)).map(&:to_s)
329
+ foreign_key = Array(reflection.foreign_key)
330
+
331
+ primary_key_foreign_key_pairs = primary_key.zip(foreign_key)
332
+ primary_key_foreign_key_pairs.each do |pk, fk|
333
+ # Notable change: add relative_id_for here
334
+ association_id = if record.class.sharded_column?(pk)
335
+ Shard.relative_id_for(
336
+ record._read_attribute(pk),
337
+ record.shard,
338
+ shard
339
+ )
340
+ else
341
+ record._read_attribute(pk)
342
+ end
343
+ self[fk] = association_id unless self[fk] == association_id
344
+ end
345
+ association.loaded!
346
+ end
347
+
348
+ saved if autosave
349
+ end
350
+ end
306
351
  end
307
352
  end
308
353
  end
@@ -57,7 +57,12 @@ module Switchman
57
57
  end
58
58
 
59
59
  def clear_query_caches_for_current_thread
60
- ::ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
60
+ pools = if ::Rails.version < "7.1"
61
+ ::ActiveRecord::Base.connection_handler.connection_pool_list
62
+ else
63
+ ::ActiveRecord::Base.connection_handler.connection_pool_list(:all)
64
+ end
65
+ pools.each do |pool|
61
66
  pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
62
67
  end
63
68
  end
@@ -158,7 +163,15 @@ module Switchman
158
163
  end
159
164
 
160
165
  @loaded_from_shard ||= Shard.current(self.class.connection_class_for_self)
161
- readonly! if shadow_record? && !Switchman.config[:writable_shadow_records]
166
+ if shadow_record? && !Switchman.config[:writable_shadow_records]
167
+ @readonly = true
168
+ @readonly_from_shadow ||= true
169
+ end
170
+ super
171
+ end
172
+
173
+ def readonly!
174
+ @readonly_from_shadow = false
162
175
  super
163
176
  end
164
177
 
@@ -169,6 +182,10 @@ module Switchman
169
182
  pkey > Shard::IDS_PER_SHARD
170
183
  end
171
184
 
185
+ def canonical?
186
+ !shadow_record?
187
+ end
188
+
172
189
  def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
173
190
  return if target_shard == shard
174
191
 
@@ -253,9 +270,9 @@ module Switchman
253
270
  result
254
271
  end
255
272
 
256
- def transaction(**kwargs, &block)
273
+ def transaction(...)
257
274
  shard.activate(self.class.connection_class_for_self) do
258
- self.class.transaction(**kwargs, &block)
275
+ self.class.transaction(...)
259
276
  end
260
277
  end
261
278
 
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ module ActiveRecord
5
+ module ConnectionHandler
6
+ def resolve_pool_config(config, connection_name, role, shard)
7
+ ret = super
8
+ # Make *all* pool configs use the same schema reflection
9
+ ret.schema_reflection = ConnectionHandler.global_schema_reflection
10
+ ret
11
+ end
12
+
13
+ def self.global_schema_reflection
14
+ @global_schema_reflection ||= ::ActiveRecord::ConnectionAdapters::SchemaReflection.new(nil)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -3,22 +3,24 @@
3
3
  module Switchman
4
4
  module ActiveRecord
5
5
  module ConnectionPool
6
- def get_schema_cache(connection)
7
- self.schema_cache ||= SharedSchemaCache.get_schema_cache(connection)
8
- self.schema_cache.connection = connection
6
+ if ::Rails.version < "7.1"
7
+ def get_schema_cache(connection)
8
+ self.schema_cache ||= SharedSchemaCache.get_schema_cache(connection)
9
+ self.schema_cache.connection = connection
9
10
 
10
- self.schema_cache
11
- end
11
+ self.schema_cache
12
+ end
12
13
 
13
- # rubocop:disable Naming/AccessorMethodName override method
14
- def set_schema_cache(cache)
15
- schema_cache = get_schema_cache(cache.connection)
14
+ # rubocop:disable Naming/AccessorMethodName override method
15
+ def set_schema_cache(cache)
16
+ schema_cache = get_schema_cache(cache.connection)
16
17
 
17
- cache.instance_variables.each do |x|
18
- schema_cache.instance_variable_set(x, cache.instance_variable_get(x))
18
+ cache.instance_variables.each do |x|
19
+ schema_cache.instance_variable_set(x, cache.instance_variable_get(x))
20
+ end
19
21
  end
22
+ # rubocop:enable Naming/AccessorMethodName override method
20
23
  end
21
- # rubocop:enable Naming/AccessorMethodName override method
22
24
 
23
25
  def default_schema
24
26
  connection unless @schemas
@@ -7,14 +7,26 @@ module Switchman
7
7
  # since all should point to the same data, even if multiple are writable
8
8
  # (Picks 'primary' since it is guaranteed to exist and switchman handles activating
9
9
  # deploy through other means)
10
- def configs_for(include_replicas: false, name: nil, **)
11
- res = super
12
- if name && !include_replicas
13
- return nil unless name.end_with?("primary")
14
- elsif !include_replicas
15
- return res.select { |config| config.name.end_with?("primary") }
10
+ if ::Rails.version < "7.1"
11
+ def configs_for(include_replicas: false, name: nil, **)
12
+ res = super
13
+ if name && !include_replicas
14
+ return nil unless name.end_with?("primary")
15
+ elsif !include_replicas
16
+ return res.select { |config| config.name.end_with?("primary") }
17
+ end
18
+ res
19
+ end
20
+ else
21
+ def configs_for(include_hidden: false, name: nil, **)
22
+ res = super
23
+ if name && !include_hidden
24
+ return nil unless name.end_with?("primary")
25
+ elsif !include_hidden
26
+ return res.select { |config| config.name.end_with?("primary") }
27
+ end
28
+ res
16
29
  end
17
- res
18
30
  end
19
31
 
20
32
  private
@@ -42,26 +42,56 @@ module Switchman
42
42
  primary_shard.activate { super }
43
43
  end
44
44
 
45
- def exists?(conditions = :none)
46
- conditions = conditions.id if ::ActiveRecord::Base === conditions
47
- return false unless conditions
45
+ if ::Rails.version < "7.1"
46
+ def exists?(conditions = :none)
47
+ conditions = conditions.id if ::ActiveRecord::Base === conditions
48
+ return false unless conditions
48
49
 
49
- relation = apply_join_dependency(eager_loading: false)
50
- return false if ::ActiveRecord::NullRelation === relation
50
+ relation = apply_join_dependency(eager_loading: false)
51
+ return false if ::ActiveRecord::NullRelation === relation
51
52
 
52
- relation = relation.except(:select, :order).select("1 AS one").limit(1)
53
+ relation = relation.except(:select, :order).select("1 AS one").limit(1)
53
54
 
54
- case conditions
55
- when Array, Hash
56
- relation = relation.where(conditions)
57
- else
58
- relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
55
+ case conditions
56
+ when Array, Hash
57
+ relation = relation.where(conditions)
58
+ else
59
+ relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
60
+ end
61
+
62
+ relation.activate do |shard_rel|
63
+ return true if connection.select_value(shard_rel.arel, "#{name} Exists")
64
+ end
65
+ false
59
66
  end
67
+ else
68
+ def exists?(conditions = :none)
69
+ return false if @none
70
+
71
+ if Base === conditions
72
+ raise ArgumentError, <<-TEXT.squish
73
+ You are passing an instance of ActiveRecord::Base to `exists?`.
74
+ Please pass the id of the object by calling `.id`.
75
+ TEXT
76
+ end
60
77
 
61
- relation.activate do |shard_rel|
62
- return true if connection.select_value(shard_rel.arel, "#{name} Exists")
78
+ return false if !conditions || limit_value == 0 # rubocop:disable Style/NumericPredicate
79
+
80
+ if eager_loading?
81
+ relation = apply_join_dependency(eager_loading: false)
82
+ return relation.exists?(conditions)
83
+ end
84
+
85
+ relation = construct_relation_for_exists(conditions)
86
+ return false if relation.where_clause.contradiction?
87
+
88
+ relation.activate do |shard_rel|
89
+ return true if skip_query_cache_if_necessary do
90
+ connection.select_rows(shard_rel.arel, "#{name} Exists?").size == 1
91
+ end
92
+ end
93
+ false
63
94
  end
64
- false
65
95
  end
66
96
  end
67
97
  end
@@ -5,8 +5,10 @@ module Switchman
5
5
  module LogSubscriber
6
6
  # sadly, have to completely replace this
7
7
  def sql(event)
8
- self.class.runtime += event.duration
9
- return unless logger.debug?
8
+ if ::Rails.version < "7.1"
9
+ self.class.runtime += event.duration
10
+ return unless logger.debug?
11
+ end
10
12
 
11
13
  payload = event.payload
12
14
 
@@ -27,7 +29,11 @@ module Switchman
27
29
  end
28
30
 
29
31
  name = colorize_payload_name(name, payload[:name])
30
- sql = color(sql, sql_color(sql), true)
32
+ sql = if ::Rails.version < "7.1"
33
+ color(sql, sql_color(sql), true)
34
+ else
35
+ color(sql, sql_color(sql), bold: true)
36
+ end
31
37
 
32
38
  debug " #{name} #{sql}#{binds}#{shard}"
33
39
  end
@@ -29,7 +29,11 @@ module Switchman
29
29
  db_name_hash = Zlib.crc32(Shard.current.name)
30
30
  shard_name_hash = ::ActiveRecord::Migrator::MIGRATOR_SALT * db_name_hash
31
31
  # Store in internalmetadata to allow other tools to be able to lock out migrations
32
- ::ActiveRecord::InternalMetadata[:migrator_advisory_lock_id] = shard_name_hash
32
+ if ::Rails.version < "7.1"
33
+ ::ActiveRecord::InternalMetadata[:migrator_advisory_lock_id] = shard_name_hash
34
+ else
35
+ ::ActiveRecord::InternalMetadata.new(connection)[:migrator_advisory_lock_id] = shard_name_hash
36
+ end
33
37
  shard_name_hash
34
38
  end
35
39
 
@@ -48,17 +52,30 @@ module Switchman
48
52
  module MigrationContext
49
53
  def migrate(...)
50
54
  connection = ::ActiveRecord::Base.connection
51
- connection_pool = ::ActiveRecord::Base.connection_pool
52
- previous_schema_cache = connection_pool.get_schema_cache(connection)
53
- temporary_schema_cache = ::ActiveRecord::ConnectionAdapters::SchemaCache.new(connection)
55
+ schema_cache_holder = ::ActiveRecord::Base.connection_pool
56
+ schema_cache_holder = schema_cache_holder.schema_reflection if ::Rails.version >= "7.1"
57
+ previous_schema_cache = if ::Rails.version < "7.1"
58
+ schema_cache_holder.get_schema_cache(connection)
59
+ else
60
+ schema_cache_holder.instance_variable_get(:@cache)
61
+ end
62
+
63
+ if ::Rails.version < "7.1"
64
+ temporary_schema_cache = ::ActiveRecord::ConnectionAdapters::SchemaCache.new(connection)
65
+
66
+ reset_column_information
67
+ schema_cache_holder.set_schema_cache(temporary_schema_cache)
68
+ else
69
+ schema_cache_holder.instance_variable_get(:@cache)
54
70
 
55
- reset_column_information
56
- connection_pool.set_schema_cache(temporary_schema_cache)
71
+ reset_column_information
72
+ schema_cache_holder.clear!
73
+ end
57
74
 
58
75
  begin
59
76
  super(...)
60
77
  ensure
61
- connection_pool.set_schema_cache(previous_schema_cache)
78
+ schema_cache_holder.set_schema_cache(previous_schema_cache)
62
79
  reset_column_information
63
80
  end
64
81
  end
@@ -34,6 +34,10 @@ module Switchman
34
34
  # When a shadow record is reloaded the real record is returned. So
35
35
  # we need to ensure the loaded_from_shard is set correctly after a reload.
36
36
  @loaded_from_shard = @shard
37
+ if @readonly_from_shadow
38
+ @readonly_from_shadow = false
39
+ @readonly = false
40
+ end
37
41
  res
38
42
  end
39
43
 
@@ -5,28 +5,57 @@ module Switchman
5
5
  module QueryCache
6
6
  private
7
7
 
8
- def cache_sql(sql, name, binds)
9
- # have to include the shard id in the cache key because of switching dbs on the same connection
10
- sql = "#{shard.id}::#{sql}"
11
- @lock.synchronize do
12
- result =
13
- if query_cache[sql].key?(binds)
14
- args = {
15
- sql: sql,
16
- binds: binds,
17
- name: name,
18
- connection_id: object_id,
19
- cached: true,
20
- type_casted_binds: -> { type_casted_binds(binds) }
21
- }
22
- ::ActiveSupport::Notifications.instrument(
23
- "sql.active_record",
24
- args
25
- )
26
- query_cache[sql][binds]
8
+ if ::Rails.version < "7.1"
9
+ def cache_sql(sql, name, binds)
10
+ # have to include the shard id in the cache key because of switching dbs on the same connection
11
+ sql = "#{shard.id}::#{sql}"
12
+ @lock.synchronize do
13
+ result =
14
+ if query_cache[sql].key?(binds)
15
+ args = {
16
+ sql: sql,
17
+ binds: binds,
18
+ name: name,
19
+ connection_id: object_id,
20
+ cached: true,
21
+ type_casted_binds: -> { type_casted_binds(binds) }
22
+ }
23
+ ::ActiveSupport::Notifications.instrument(
24
+ "sql.active_record",
25
+ args
26
+ )
27
+ query_cache[sql][binds]
28
+ else
29
+ query_cache[sql][binds] = yield
30
+ end
31
+ result.dup
32
+ end
33
+ end
34
+ else
35
+ def cache_sql(sql, name, binds)
36
+ # have to include the shard id in the cache key because of switching dbs on the same connection
37
+ sql = "#{shard.id}::#{sql}"
38
+ key = binds.empty? ? sql : [sql, binds]
39
+ result = nil
40
+ hit = false
41
+
42
+ @lock.synchronize do
43
+ if (result = @query_cache.delete(key))
44
+ hit = true
45
+ @query_cache[key] = result
27
46
  else
28
- query_cache[sql][binds] = yield
47
+ result = @query_cache[key] = yield
48
+ @query_cache.shift if @query_cache_max_size && @query_cache.size > @query_cache_max_size
29
49
  end
50
+ end
51
+
52
+ if hit
53
+ ::ActiveSupport::Notifications.instrument(
54
+ "sql.active_record",
55
+ cache_notification_info(sql, name, binds)
56
+ )
57
+ end
58
+
30
59
  result.dup
31
60
  end
32
61
  end
@@ -175,7 +175,7 @@ module Switchman
175
175
  end
176
176
 
177
177
  def sharded_foreign_key?(relation, column)
178
- models_for_table(relation.table_name).any? { |m| m.sharded_column?(column) }
178
+ models_for_table(relation.name).any? { |m| m.sharded_column?(column) }
179
179
  end
180
180
 
181
181
  def sharded_primary_key?(relation, column)
@@ -187,7 +187,7 @@ module Switchman
187
187
 
188
188
  def source_shard_for_foreign_key(relation, column)
189
189
  reflection = nil
190
- models_for_table(relation.table_name).each do |model|
190
+ models_for_table(relation.name).each do |model|
191
191
  reflection = model.send(:reflection_for_integer_attribute, column)
192
192
  break if reflection
193
193
  end
@@ -14,21 +14,28 @@ module Switchman
14
14
  # Code adapted from the code in rails proper
15
15
  @connection_subscriber =
16
16
  ::ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload|
17
- spec_name = payload[:spec_name] if payload.key?(:spec_name)
17
+ spec_name = if ::Rails.version < "7.1"
18
+ payload[:spec_name] if payload.key?(:spec_name)
19
+ elsif payload.key?(:connection_name)
20
+ payload[:connection_name]
21
+ end
18
22
  shard = payload[:shard] if payload.key?(:shard)
19
- setup_shared_connection_pool
20
23
 
21
24
  if spec_name && !FORBIDDEN_DB_ENVS.include?(shard)
22
25
  begin
23
26
  connection = ::ActiveRecord::Base.connection_handler.retrieve_connection(spec_name, shard: shard)
27
+ connection.connect! if ::Rails.version >= "7.1" # eagerly validate the connection
24
28
  rescue ::ActiveRecord::ConnectionNotEstablished, ::ActiveRecord::NoDatabaseError
25
29
  connection = nil
26
30
  end
27
31
 
28
- if connection && !@fixture_connections.include?(connection)
29
- connection.begin_transaction joinable: false, _lazy: false
30
- connection.pool.lock_thread = true if lock_threads
31
- @fixture_connections << connection
32
+ if connection
33
+ setup_shared_connection_pool
34
+ unless @fixture_connections.include?(connection)
35
+ connection.begin_transaction joinable: false, _lazy: false
36
+ connection.pool.lock_thread = true if lock_threads
37
+ @fixture_connections << connection
38
+ end
32
39
  end
33
40
  end
34
41
  end
@@ -37,7 +44,7 @@ module Switchman
37
44
  def enlist_fixture_connections
38
45
  setup_shared_connection_pool
39
46
 
40
- ::ActiveRecord::Base.connection_handler.connection_pool_list.reject do |cp|
47
+ ::ActiveRecord::Base.connection_handler.connection_pool_list(:primary).reject do |cp|
41
48
  FORBIDDEN_DB_ENVS.include?(cp.db_config.env_name.to_sym)
42
49
  end.map(&:connection)
43
50
  end
@@ -13,6 +13,20 @@ 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
32
  collector << " "
@@ -24,27 +38,29 @@ module Switchman
24
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
@@ -10,6 +10,10 @@ 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
@@ -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)
@@ -140,6 +148,29 @@ module Switchman
140
148
  end
141
149
  end
142
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
+
143
174
  # locks this db to a specific environment, except for
144
175
  # when doing writes (then it falls back to the current
145
176
  # value of GuardRail.environment)
@@ -228,7 +259,11 @@ module Switchman
228
259
  unless schema == false
229
260
  shard.activate do
230
261
  ::ActiveRecord::Base.connection.transaction(requires_new: true) do
231
- ::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
232
267
  end
233
268
 
234
269
  ::ActiveRecord::Base.descendants.reject do |m|
@@ -6,6 +6,7 @@ module Switchman
6
6
  "default"
7
7
  end
8
8
  alias_method :cache_key, :id
9
+
9
10
  def activate(*_classes)
10
11
  yield
11
12
  end
@@ -57,6 +58,16 @@ 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
@@ -5,7 +5,7 @@ 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)
@@ -60,6 +60,9 @@ module Switchman
60
60
  )
61
61
  end
62
62
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::AbstractAdapter)
63
+ unless ::Rails.version < "7.1"
64
+ ::ActiveRecord::ConnectionAdapters::ConnectionHandler.prepend(ActiveRecord::ConnectionHandler)
65
+ end
63
66
  ::ActiveRecord::ConnectionAdapters::ConnectionPool.prepend(ActiveRecord::ConnectionPool)
64
67
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::QueryCache)
65
68
  ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(ActiveRecord::PostgreSQLAdapter)
@@ -121,7 +121,7 @@ module Switchman
121
121
  next if @@sharding_failed
122
122
 
123
123
  # clean up after specs
124
- DatabaseServer.all.each do |ds|
124
+ DatabaseServer.each do |ds|
125
125
  if ds.fake? && ds != @shard2.database_server
126
126
  ds.shards.delete_all unless use_transactional_tests
127
127
  ds.destroy
@@ -13,6 +13,47 @@ module Switchman
13
13
 
14
14
  scope :primary, -> { where(name: nil).order(:database_server_id, :id).distinct_on(:database_server_id) }
15
15
 
16
+ scope :in_region, (lambda do |region, include_regionless: true|
17
+ next in_current_region if region.nil?
18
+
19
+ dbs_by_region = DatabaseServer.group_by(&:region)
20
+ db_count_in_this_region = dbs_by_region[region]&.length.to_i
21
+ db_count_in_this_region += dbs_by_region[nil]&.length.to_i if include_regionless
22
+ non_existent_database_servers = Shard.send(:non_existent_database_servers)
23
+ db_count_in_other_regions = DatabaseServer.all.length -
24
+ db_count_in_this_region +
25
+ non_existent_database_servers.length
26
+
27
+ dbs_in_this_region = dbs_by_region[region]&.map(&:id) || []
28
+ dbs_in_this_region += dbs_by_region[nil]&.map(&:id) || [] if include_regionless
29
+
30
+ if db_count_in_this_region <= db_count_in_other_regions
31
+ if dbs_in_this_region.include?(Shard.default.database_server.id)
32
+ where("database_server_id IN (?) OR database_server_id IS NULL", dbs_in_this_region)
33
+ else
34
+ where(database_server_id: dbs_in_this_region)
35
+ end
36
+ elsif db_count_in_other_regions.zero?
37
+ all
38
+ else
39
+ dbs_not_in_this_region = DatabaseServer.map(&:id) - dbs_in_this_region + non_existent_database_servers
40
+ if dbs_in_this_region.include?(Shard.default.database_server.id)
41
+ where("database_server_id NOT IN (?) OR database_server_id IS NULL", dbs_not_in_this_region)
42
+ else
43
+ where.not(database_server_id: dbs_not_in_this_region)
44
+ end
45
+ end
46
+ end)
47
+
48
+ scope :in_current_region, (lambda do |include_regionless: true|
49
+ # sharding isn't set up? maybe we're in tests, or a somehow degraded environment
50
+ # either way there's only one shard, and we always want to see it
51
+ return [default] unless default.is_a?(Switchman::Shard)
52
+ return all if !Switchman.region || DatabaseServer.none?(&:region)
53
+
54
+ in_region(Switchman.region, include_regionless: include_regionless)
55
+ end)
56
+
16
57
  class << self
17
58
  def sharded_models
18
59
  @sharded_models ||= [::ActiveRecord::Base, UnshardedRecord].freeze
@@ -166,7 +207,11 @@ module Switchman
166
207
  # clear connections prior to forking (no more queries will be executed in the parent,
167
208
  # and we want them gone so that we don't accidentally use them post-fork doing something
168
209
  # silly like dealloc'ing prepared statements)
169
- ::ActiveRecord::Base.clear_all_connections!
210
+ if ::Rails.version < "7.1"
211
+ ::ActiveRecord::Base.clear_all_connections!(nil)
212
+ else
213
+ ::ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
214
+ end
170
215
 
171
216
  parent_process_name = sanitized_process_title
172
217
  ret = ::Parallel.map(scopes, in_processes: (scopes.length > 1) ? parallel : 0) do |server, subscope|
@@ -258,7 +303,7 @@ module Switchman
258
303
  else
259
304
  shard = partition_object.shard
260
305
  end
261
- when Integer, /^\d+$/, /^(\d+)~(\d+)$/
306
+ when Integer, /\A\d+\Z/, /\A(\d+)~(\d+)\Z/
262
307
  local_id, shard = Shard.local_id_for(partition_object)
263
308
  local_id ||= partition_object
264
309
  object = local_id unless partition_proc
@@ -300,14 +345,14 @@ module Switchman
300
345
  case any_id
301
346
  when ::ActiveRecord::Base
302
347
  any_id.id
303
- when /^(\d+)~(-?\d+)$/
348
+ when /\A(\d+)~(-?\d+)\Z/
304
349
  local_id = $2.to_i
305
350
  signed_id_operation(local_id) do |id|
306
351
  return nil if id > IDS_PER_SHARD
307
352
 
308
353
  ($1.to_i * IDS_PER_SHARD) + id
309
354
  end
310
- when Integer, /^-?\d+$/
355
+ when Integer, /\A-?\d+\Z/
311
356
  any_id.to_i
312
357
  end
313
358
  end
@@ -391,7 +436,7 @@ module Switchman
391
436
  end
392
437
 
393
438
  def configure_connects_to
394
- full_connects_to_hash = DatabaseServer.all.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
439
+ full_connects_to_hash = DatabaseServer.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
395
440
  sharded_models.each do |klass|
396
441
  connects_to_hash = full_connects_to_hash.deep_dup
397
442
  if klass == UnshardedRecord
@@ -410,7 +455,10 @@ module Switchman
410
455
  end
411
456
  end
412
457
 
458
+ # this resets the default shard on rails 7.1+, but we want to preserve it
459
+ shard_was = klass.default_shard
413
460
  klass.connects_to shards: connects_to_hash
461
+ klass.default_shard = shard_was
414
462
  end
415
463
  end
416
464
 
@@ -477,8 +525,17 @@ module Switchman
477
525
  argv[0] = File.basename(argv[0])
478
526
  argv.shelljoin
479
527
  end
528
+
529
+ # @return [Array<String>] the list of database servers that are in the
530
+ # config, but don't have any shards on them
531
+ def non_existent_database_servers
532
+ @non_existent_database_servers ||=
533
+ Shard.distinct.pluck(:database_server_id).compact - DatabaseServer.all.map(&:id)
534
+ end
480
535
  end
481
536
 
537
+ delegate :region, :in_region?, :in_current_region?, to: :database_server
538
+
482
539
  def name
483
540
  unless instance_variable_defined?(:@name)
484
541
  # protect against re-entrancy
@@ -519,6 +576,10 @@ module Switchman
519
576
  id
520
577
  end
521
578
 
579
+ def original_id_value
580
+ id
581
+ end
582
+
522
583
  def activate(*classes, &block)
523
584
  shards = hashify_classes(classes)
524
585
  Shard.activate(shards, &block)
@@ -13,8 +13,10 @@ module Switchman
13
13
  Thread.current[:switchman_error_handler] = true
14
14
 
15
15
  @active_shards ||= Shard.active_shards
16
- rescue ThreadError # e.g. `require': can't be called from trap context (ThreadError)
17
- # intentionally empty
16
+ rescue
17
+ # intentionally empty - don't allow calculating the active_shards to prevent
18
+ # creating the StandardError for any reason. this prevents various random issues
19
+ # when a StandardError is created within a finalizer
18
20
  ensure
19
21
  Thread.current[:switchman_error_handler] = nil
20
22
  end
@@ -19,7 +19,7 @@ module Switchman
19
19
  end
20
20
 
21
21
  server1 = Shard.default.database_server
22
- server2 = DatabaseServer.create(Shard.default.database_server.config.merge(server2: true))
22
+ server2 = DatabaseServer.create(Shard.default.database_server.config.merge(server2: true, schema_dump: false))
23
23
 
24
24
  if server1 == Shard.default.database_server && server1.config[:shard1] && server1.config[:shard2]
25
25
  # look for the shards in the db already
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- VERSION = "3.5.9"
4
+ VERSION = "3.5.16"
5
5
  end
data/lib/switchman.rb CHANGED
@@ -20,27 +20,31 @@ loader.setup
20
20
  module Switchman
21
21
  Deprecation = ::ActiveSupport::Deprecation.new("4.0", "Switchman")
22
22
 
23
- def self.config
24
- # TODO: load from yaml
25
- @config ||= {}
26
- end
23
+ class << self
24
+ attr_writer :cache
27
25
 
28
- def self.cache
29
- (@cache.respond_to?(:call) ? @cache.call : @cache) || ::Rails.cache
30
- end
26
+ def config
27
+ # TODO: load from yaml
28
+ @config ||= {}
29
+ end
31
30
 
32
- def self.cache=(cache)
33
- @cache = cache
34
- end
31
+ def cache
32
+ (@cache.respond_to?(:call) ? @cache.call : @cache) || ::Rails.cache
33
+ end
34
+
35
+ def region
36
+ config[:region]
37
+ end
35
38
 
36
- def self.foreign_key_check(name, type, limit: nil)
37
- return unless name.to_s.end_with?("_id") && type.to_s == "integer" && limit.to_i < 8
39
+ def foreign_key_check(name, type, limit: nil)
40
+ return unless name.to_s.end_with?("_id") && type.to_s == "integer" && limit.to_i < 8
38
41
 
39
- puts <<~TEXT.squish
40
- WARNING: All foreign keys need to be 8-byte integers.
41
- #{name} looks like a foreign key.
42
- If so, please add the option: `:limit => 8`
43
- TEXT
42
+ puts <<~TEXT.squish
43
+ WARNING: All foreign keys need to be 8-byte integers.
44
+ #{name} looks like a foreign key.
45
+ If so, please add the option: `:limit => 8`
46
+ TEXT
47
+ end
44
48
  end
45
49
 
46
50
  class OrderOnMultiShardQuery < RuntimeError; end
@@ -35,7 +35,7 @@ module Switchman
35
35
 
36
36
  servers = servers.filter_map { |server| DatabaseServer.find(server) }
37
37
  if open
38
- open_servers = DatabaseServer.all.select { |server| server.config[:open] }
38
+ open_servers = DatabaseServer.select { |server| server.config[:open] }
39
39
  servers.concat(open_servers)
40
40
  servers << DatabaseServer.find(nil) if open_servers.empty?
41
41
  servers.uniq!
@@ -43,6 +43,19 @@ module Switchman
43
43
  servers = DatabaseServer.all - servers if negative
44
44
  end
45
45
 
46
+ ENV["REGION"]&.split(",")&.each do |region|
47
+ method = :select!
48
+ if region[0] == "-"
49
+ method = :reject!
50
+ region = region[1..]
51
+ end
52
+ if region == "self"
53
+ servers.send(method, &:in_current_region?)
54
+ else
55
+ servers.send(method) { |server| server.in_region?(region) }
56
+ end
57
+ end
58
+
46
59
  servers = filter_database_servers_chain.call(servers)
47
60
 
48
61
  scope = base_scope.order(::Arel.sql("database_server_id IS NOT NULL, database_server_id, id"))
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switchman
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.5.9
4
+ version: 3.5.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2023-08-23 00:00:00.000000000 Z
13
+ date: 2023-10-17 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -21,7 +21,7 @@ dependencies:
21
21
  version: 6.1.4
22
22
  - - "<"
23
23
  - !ruby/object:Gem::Version
24
- version: '7.1'
24
+ version: '7.2'
25
25
  type: :runtime
26
26
  prerelease: false
27
27
  version_requirements: !ruby/object:Gem::Requirement
@@ -31,7 +31,7 @@ dependencies:
31
31
  version: 6.1.4
32
32
  - - "<"
33
33
  - !ruby/object:Gem::Version
34
- version: '7.1'
34
+ version: '7.2'
35
35
  - !ruby/object:Gem::Dependency
36
36
  name: guardrail
37
37
  requirement: !ruby/object:Gem::Requirement
@@ -69,7 +69,7 @@ dependencies:
69
69
  version: '6.1'
70
70
  - - "<"
71
71
  - !ruby/object:Gem::Version
72
- version: '7.1'
72
+ version: '7.2'
73
73
  type: :runtime
74
74
  prerelease: false
75
75
  version_requirements: !ruby/object:Gem::Requirement
@@ -79,35 +79,21 @@ dependencies:
79
79
  version: '6.1'
80
80
  - - "<"
81
81
  - !ruby/object:Gem::Version
82
- version: '7.1'
82
+ version: '7.2'
83
83
  - !ruby/object:Gem::Dependency
84
- name: appraisal
84
+ name: debug
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '2.3'
89
+ version: '1.8'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '2.3'
97
- - !ruby/object:Gem::Dependency
98
- name: byebug
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
96
+ version: '1.8'
111
97
  - !ruby/object:Gem::Dependency
112
98
  name: pg
113
99
  requirement: !ruby/object:Gem::Requirement
@@ -122,20 +108,6 @@ dependencies:
122
108
  - - "~>"
123
109
  - !ruby/object:Gem::Version
124
110
  version: '1.2'
125
- - !ruby/object:Gem::Dependency
126
- name: pry
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
111
  - !ruby/object:Gem::Dependency
140
112
  name: rake
141
113
  requirement: !ruby/object:Gem::Requirement
@@ -269,6 +241,7 @@ files:
269
241
  - lib/switchman/active_record/attribute_methods.rb
270
242
  - lib/switchman/active_record/base.rb
271
243
  - lib/switchman/active_record/calculations.rb
244
+ - lib/switchman/active_record/connection_handler.rb
272
245
  - lib/switchman/active_record/connection_pool.rb
273
246
  - lib/switchman/active_record/database_configurations.rb
274
247
  - lib/switchman/active_record/database_configurations/database_config.rb