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 +4 -4
- data/lib/switchman/active_record/abstract_adapter.rb +4 -2
- data/lib/switchman/active_record/associations.rb +50 -5
- data/lib/switchman/active_record/base.rb +21 -4
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +13 -11
- data/lib/switchman/active_record/database_configurations.rb +19 -7
- data/lib/switchman/active_record/finder_methods.rb +44 -14
- data/lib/switchman/active_record/log_subscriber.rb +9 -3
- data/lib/switchman/active_record/migration.rb +24 -7
- data/lib/switchman/active_record/persistence.rb +4 -0
- data/lib/switchman/active_record/query_cache.rb +49 -20
- data/lib/switchman/active_record/query_methods.rb +2 -2
- data/lib/switchman/active_record/test_fixtures.rb +14 -7
- data/lib/switchman/arel.rb +32 -16
- data/lib/switchman/database_server.rb +36 -1
- data/lib/switchman/default_shard.rb +11 -0
- data/lib/switchman/engine.rb +4 -1
- data/lib/switchman/r_spec_helper.rb +1 -1
- data/lib/switchman/shard.rb +66 -5
- data/lib/switchman/standard_error.rb +4 -2
- data/lib/switchman/test_helper.rb +1 -1
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +21 -17
- data/lib/tasks/switchman.rake +14 -1
- metadata +10 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a27299a15c04348fc6a632a8c3c53b8c4fa541140b8befd07f63bc3544f01f50
|
4
|
+
data.tar.gz: d9a43fc82c874a87026c5d0b9467a45ba71df19e0c94166f22cc1aabe11b8e5f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b47b69ec2830ed31204d0fe122d555b9f6623683469d6b35329cc1bfe1a18b6e5ec769985d792f64c72a696a4a16f4119407a9323b2560f4be575f52d5c09b9
|
7
|
+
data.tar.gz: 19bd4da4fa0ba4e4ec03c57858481f6dddd799b98bcd9e67e3870bbfb60c7cb69d44ba74781788c084069dc5bc17632f74b3c793d1964a8ef76122f2f78c144e
|
@@ -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
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
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
|
-
::
|
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
|
-
|
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(
|
273
|
+
def transaction(...)
|
257
274
|
shard.activate(self.class.connection_class_for_self) do
|
258
|
-
self.class.transaction(
|
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
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
11
|
-
|
11
|
+
self.schema_cache
|
12
|
+
end
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
# rubocop:disable Naming/AccessorMethodName override method
|
15
|
+
def set_schema_cache(cache)
|
16
|
+
schema_cache = get_schema_cache(cache.connection)
|
16
17
|
|
17
|
-
|
18
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
46
|
-
conditions =
|
47
|
-
|
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
|
-
|
50
|
-
|
50
|
+
relation = apply_join_dependency(eager_loading: false)
|
51
|
+
return false if ::ActiveRecord::NullRelation === relation
|
51
52
|
|
52
|
-
|
53
|
+
relation = relation.except(:select, :order).select("1 AS one").limit(1)
|
53
54
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
62
|
-
|
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
|
-
|
9
|
-
|
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 =
|
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
|
-
::
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
56
|
-
|
71
|
+
reset_column_information
|
72
|
+
schema_cache_holder.clear!
|
73
|
+
end
|
57
74
|
|
58
75
|
begin
|
59
76
|
super(...)
|
60
77
|
ensure
|
61
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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[
|
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.
|
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.
|
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 =
|
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
|
29
|
-
|
30
|
-
connection
|
31
|
-
|
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
|
data/lib/switchman/arel.rb
CHANGED
@@ -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
|
-
|
28
|
-
|
41
|
+
if ::Rails.version < "7.1"
|
42
|
+
def visit_Arel_Nodes_HomogeneousIn(o, collector)
|
43
|
+
collector.preparable = false
|
29
44
|
|
30
|
-
|
45
|
+
collector << quote_local_table_name(o.table_name) << "." << quote_column_name(o.column_name)
|
31
46
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
47
|
+
collector << if o.type == :in
|
48
|
+
" IN ("
|
49
|
+
else
|
50
|
+
" NOT IN ("
|
51
|
+
end
|
37
52
|
|
38
|
-
|
53
|
+
values = o.casted_values
|
39
54
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
47
|
-
|
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
|
-
::
|
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
|
data/lib/switchman/engine.rb
CHANGED
@@ -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.
|
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
|
data/lib/switchman/shard.rb
CHANGED
@@ -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
|
-
::
|
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,
|
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
|
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,
|
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.
|
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
|
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
|
data/lib/switchman/version.rb
CHANGED
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
|
-
|
24
|
-
|
25
|
-
@config ||= {}
|
26
|
-
end
|
23
|
+
class << self
|
24
|
+
attr_writer :cache
|
27
25
|
|
28
|
-
|
29
|
-
|
30
|
-
|
26
|
+
def config
|
27
|
+
# TODO: load from yaml
|
28
|
+
@config ||= {}
|
29
|
+
end
|
31
30
|
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
37
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
data/lib/tasks/switchman.rake
CHANGED
@@ -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.
|
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.
|
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-
|
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.
|
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.
|
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.
|
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.
|
82
|
+
version: '7.2'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: debug
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
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: '
|
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
|