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 +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
|