switchman 3.5.9 → 3.5.16

Sign up to get free protection for your applications and to get access to all the features.
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