switchman 3.4.2 → 3.6.7
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/Rakefile +15 -14
- data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
- data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
- data/lib/switchman/active_record/abstract_adapter.rb +4 -2
- data/lib/switchman/active_record/associations.rb +89 -16
- data/lib/switchman/active_record/attribute_methods.rb +67 -22
- data/lib/switchman/active_record/base.rb +112 -22
- data/lib/switchman/active_record/calculations.rb +93 -37
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +18 -14
- data/lib/switchman/active_record/database_configurations.rb +37 -15
- data/lib/switchman/active_record/finder_methods.rb +44 -14
- data/lib/switchman/active_record/log_subscriber.rb +11 -5
- data/lib/switchman/active_record/migration.rb +28 -9
- data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
- data/lib/switchman/active_record/persistence.rb +22 -0
- data/lib/switchman/active_record/postgresql_adapter.rb +11 -10
- data/lib/switchman/active_record/predicate_builder.rb +2 -2
- data/lib/switchman/active_record/query_cache.rb +49 -20
- data/lib/switchman/active_record/query_methods.rb +93 -30
- data/lib/switchman/active_record/relation.rb +22 -11
- data/lib/switchman/active_record/spawn_methods.rb +2 -2
- data/lib/switchman/active_record/statement_cache.rb +2 -2
- data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
- data/lib/switchman/active_record/test_fixtures.rb +26 -16
- data/lib/switchman/active_support/cache.rb +9 -4
- data/lib/switchman/arel.rb +34 -18
- data/lib/switchman/call_super.rb +2 -8
- data/lib/switchman/database_server.rb +68 -21
- data/lib/switchman/default_shard.rb +14 -3
- data/lib/switchman/engine.rb +39 -19
- data/lib/switchman/environment.rb +2 -2
- data/lib/switchman/errors.rb +4 -1
- data/lib/switchman/guard_rail/relation.rb +1 -2
- data/lib/switchman/parallel.rb +5 -5
- data/lib/switchman/r_spec_helper.rb +11 -11
- data/lib/switchman/shard.rb +166 -64
- data/lib/switchman/sharded_instrumenter.rb +7 -3
- data/lib/switchman/standard_error.rb +4 -0
- data/lib/switchman/test_helper.rb +2 -2
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +27 -15
- data/lib/tasks/switchman.rake +117 -51
- metadata +19 -44
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e86d08703a350aa5b8d3d843c51a0813f00e6ee0c681d0c95094f84863b327b
|
4
|
+
data.tar.gz: f13e438d68d996f55c9be543cc8cb89967b62f9ddd111ab4fae188da8b959096
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7be63615d79be0b46184b9e6754bcb6091873e2eb0c4722705daea7dd81dd50320819b2238538c818637901570c2ad47a7ed62febd441c4d5b130c78391554cc
|
7
|
+
data.tar.gz: c3eb6290f9437ccac7162216840c2f258f31fc6c978121b9054c6308640bff6d522f2f61fd2247fea178c1c173a012a4d7f2c22b2e234d7b4f431ce4b8306403
|
data/Rakefile
CHANGED
@@ -2,37 +2,38 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
begin
|
5
|
-
require
|
5
|
+
require "bundler/setup"
|
6
6
|
rescue LoadError
|
7
|
-
puts
|
7
|
+
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
8
8
|
end
|
9
9
|
begin
|
10
|
-
require
|
10
|
+
require "rdoc/task"
|
11
11
|
rescue LoadError
|
12
|
-
require
|
13
|
-
require
|
12
|
+
require "rdoc/rdoc"
|
13
|
+
require "rake/rdoctask"
|
14
14
|
RDoc::Task = Rake::RDocTask
|
15
15
|
end
|
16
16
|
|
17
17
|
RDoc::Task.new(:rdoc) do |rdoc|
|
18
|
-
rdoc.rdoc_dir =
|
19
|
-
rdoc.title =
|
20
|
-
rdoc.options <<
|
21
|
-
rdoc.rdoc_files.include(
|
18
|
+
rdoc.rdoc_dir = "rdoc"
|
19
|
+
rdoc.title = "Switchman"
|
20
|
+
rdoc.options << "--line-numbers"
|
21
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
22
22
|
end
|
23
23
|
|
24
|
-
|
25
|
-
|
24
|
+
load "./spec/tasks/coverage.rake"
|
25
|
+
APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
|
26
|
+
load "rails/tasks/engine.rake"
|
26
27
|
|
27
28
|
Bundler::GemHelper.install_tasks
|
28
29
|
|
29
|
-
require
|
30
|
+
require "rspec/core/rake_task"
|
30
31
|
RSpec::Core::RakeTask.new
|
31
32
|
|
32
|
-
require
|
33
|
+
require "rubocop/rake_task"
|
33
34
|
|
34
35
|
RuboCop::RakeTask.new do |task|
|
35
|
-
task.options = [
|
36
|
+
task.options = ["-S"]
|
36
37
|
end
|
37
38
|
|
38
39
|
task default: %i[spec]
|
@@ -5,7 +5,7 @@ class AddDefaultShardIndex < ActiveRecord::Migration[4.2]
|
|
5
5
|
Switchman::Shard.where(default: nil).update_all(default: false) if Switchman::Shard.current.default?
|
6
6
|
change_column_default :switchman_shards, :default, false
|
7
7
|
change_column_null :switchman_shards, :default, false
|
8
|
-
options = if connection.adapter_name ==
|
8
|
+
options = if connection.adapter_name == "PostgreSQL"
|
9
9
|
{ unique: true, where: '"default"' }
|
10
10
|
else
|
11
11
|
{}
|
@@ -3,9 +3,15 @@
|
|
3
3
|
class AddUniqueNameIndexes < ActiveRecord::Migration[4.2]
|
4
4
|
def change
|
5
5
|
add_index :switchman_shards, %i[database_server_id name], unique: true
|
6
|
-
add_index :switchman_shards,
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
add_index :switchman_shards,
|
7
|
+
:database_server_id,
|
8
|
+
unique: true,
|
9
|
+
where: "name IS NULL",
|
10
|
+
name: "index_switchman_shards_unique_primary_shard"
|
11
|
+
add_index :switchman_shards,
|
12
|
+
"(true)",
|
13
|
+
unique: true,
|
14
|
+
where: "database_server_id IS NULL AND name IS NULL",
|
15
|
+
name: "index_switchman_shards_unique_primary_db_and_shard"
|
10
16
|
end
|
11
17
|
end
|
@@ -24,11 +24,23 @@ module Switchman
|
|
24
24
|
|
25
25
|
module CollectionAssociation
|
26
26
|
def find_target
|
27
|
-
shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards)
|
27
|
+
shards = if reflection.options[:multishard] && owner.respond_to?(:associated_shards)
|
28
|
+
owner.associated_shards
|
29
|
+
else
|
30
|
+
[shard]
|
31
|
+
end
|
28
32
|
# activate both the owner and the target's shard category, so that Reflection#join_id_for,
|
29
33
|
# when called for the owner, will be returned relative to shard the query will execute on
|
30
|
-
Shard.with_each_shard(shards,
|
31
|
-
|
34
|
+
Shard.with_each_shard(shards,
|
35
|
+
[klass.connection_class_for_self, owner.class.connection_class_for_self].uniq) do
|
36
|
+
if reflection.options[:multishard] && owner.respond_to?(:associated_shards) && reflection.has_scope?
|
37
|
+
# Prevent duplicate results when reflection has a scope (when it would use the skip_statement_cache? path)
|
38
|
+
# otherwise, the super call will set the shard_value to the object, causing it to iterate too many times
|
39
|
+
# over the associated shards
|
40
|
+
scope.shard(Shard.current(scope.klass.connection_class_for_self), :association).to_a
|
41
|
+
else
|
42
|
+
super
|
43
|
+
end
|
32
44
|
end
|
33
45
|
end
|
34
46
|
|
@@ -128,7 +140,12 @@ module Switchman
|
|
128
140
|
Shard.lookup(shard).activate do
|
129
141
|
scope_was = loader_query.scope
|
130
142
|
begin
|
131
|
-
loader_query.instance_variable_set(
|
143
|
+
loader_query.instance_variable_set(
|
144
|
+
:@scope,
|
145
|
+
loader_query.scope.shard(
|
146
|
+
Shard.current(loader_query.scope.model.connection_class_for_self)
|
147
|
+
)
|
148
|
+
)
|
132
149
|
ret += loader_query.load_records_for_keys(keys) do |record|
|
133
150
|
loaders.each { |l| l.set_inverse(record) }
|
134
151
|
end
|
@@ -186,7 +203,7 @@ module Switchman
|
|
186
203
|
# #compare_by_identity makes such owners different hash keys
|
187
204
|
@records_by_owner = {}.compare_by_identity
|
188
205
|
|
189
|
-
if ::Rails.version >=
|
206
|
+
if ::Rails.version >= "7.0"
|
190
207
|
raw_records ||= loader_query.records_for([self])
|
191
208
|
elsif owner_keys.empty?
|
192
209
|
raw_records ||= []
|
@@ -210,7 +227,8 @@ module Switchman
|
|
210
227
|
relative_owner_keys = partitioned_owners.map do |owner|
|
211
228
|
key = owner[owner_key_name]
|
212
229
|
if key && owner.class.sharded_column?(owner_key_name)
|
213
|
-
key = Shard.relative_id_for(key,
|
230
|
+
key = Shard.relative_id_for(key,
|
231
|
+
owner.shard,
|
214
232
|
Shard.current(klass.connection_class_for_self))
|
215
233
|
end
|
216
234
|
convert_key(key)
|
@@ -273,18 +291,73 @@ module Switchman
|
|
273
291
|
end
|
274
292
|
|
275
293
|
module AutosaveAssociation
|
276
|
-
|
277
|
-
|
294
|
+
if ::Rails.version < "7.1"
|
295
|
+
def association_foreign_key_changed?(reflection, record, key)
|
296
|
+
return false if reflection.through_reflection?
|
278
297
|
|
279
|
-
|
280
|
-
|
281
|
-
|
298
|
+
# have to use send instead of _read_attribute because sharding
|
299
|
+
record.has_attribute?(reflection.foreign_key) && record.send(reflection.foreign_key) != key
|
300
|
+
end
|
301
|
+
|
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 association_foreign_key_changed?(reflection, record, key)
|
310
|
+
return false if reflection.through_reflection?
|
311
|
+
|
312
|
+
foreign_key = Array(reflection.foreign_key)
|
313
|
+
return false unless foreign_key.all? { |k| record._has_attribute?(k) }
|
314
|
+
|
315
|
+
# have to use send instead of _read_attribute because sharding
|
316
|
+
foreign_key.map { |k| record.send(k) } != Array(key)
|
317
|
+
end
|
282
318
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
319
|
+
def save_belongs_to_association(reflection)
|
320
|
+
association = association_instance_get(reflection.name)
|
321
|
+
return unless association&.loaded? && !association.stale_target?
|
322
|
+
|
323
|
+
record = association.load_target
|
324
|
+
return unless record && !record.destroyed?
|
325
|
+
|
326
|
+
autosave = reflection.options[:autosave]
|
327
|
+
|
328
|
+
if autosave && record.marked_for_destruction?
|
329
|
+
foreign_key = Array(reflection.foreign_key)
|
330
|
+
foreign_key.each { |key| self[key] = nil }
|
331
|
+
record.destroy
|
332
|
+
elsif autosave != false
|
333
|
+
if record.new_record? || (autosave && record.changed_for_autosave?)
|
334
|
+
saved = record.save(validate: !autosave)
|
335
|
+
end
|
336
|
+
|
337
|
+
if association.updated?
|
338
|
+
primary_key = Array(compute_primary_key(reflection, record)).map(&:to_s)
|
339
|
+
foreign_key = Array(reflection.foreign_key)
|
340
|
+
|
341
|
+
primary_key_foreign_key_pairs = primary_key.zip(foreign_key)
|
342
|
+
primary_key_foreign_key_pairs.each do |pk, fk|
|
343
|
+
# Notable change: add relative_id_for here
|
344
|
+
association_id = if record.class.sharded_column?(pk)
|
345
|
+
Shard.relative_id_for(
|
346
|
+
record._read_attribute(pk),
|
347
|
+
record.shard,
|
348
|
+
shard
|
349
|
+
)
|
350
|
+
else
|
351
|
+
record._read_attribute(pk)
|
352
|
+
end
|
353
|
+
self[fk] = association_id unless self[fk] == association_id
|
354
|
+
end
|
355
|
+
association.loaded!
|
356
|
+
end
|
357
|
+
|
358
|
+
saved if autosave
|
359
|
+
end
|
360
|
+
end
|
288
361
|
end
|
289
362
|
end
|
290
363
|
end
|
@@ -31,8 +31,16 @@ module Switchman
|
|
31
31
|
# and not the silly one in AR::AttributeMethods::PrimaryKey
|
32
32
|
return unless sharded_column?(@primary_key)
|
33
33
|
|
34
|
-
class_eval(
|
35
|
-
|
34
|
+
class_eval(
|
35
|
+
build_sharded_getter("id",
|
36
|
+
"_read_attribute(@primary_key)",
|
37
|
+
"::#{connection_class_for_self.name}"),
|
38
|
+
__FILE__,
|
39
|
+
__LINE__
|
40
|
+
)
|
41
|
+
class_eval(build_sharded_setter("id", @primary_key, "::#{connection_class_for_self.name}"),
|
42
|
+
__FILE__,
|
43
|
+
__LINE__)
|
36
44
|
end
|
37
45
|
|
38
46
|
protected
|
@@ -47,7 +55,7 @@ module Switchman
|
|
47
55
|
end
|
48
56
|
|
49
57
|
def define_cached_method(owner, name, namespace:, as:, &block)
|
50
|
-
if ::Rails.version <
|
58
|
+
if ::Rails.version < "7.0"
|
51
59
|
yield owner
|
52
60
|
owner.rename_method(as, name)
|
53
61
|
else
|
@@ -57,7 +65,10 @@ module Switchman
|
|
57
65
|
|
58
66
|
def define_method_global_attribute(attr_name, owner:)
|
59
67
|
if sharded_column?(attr_name)
|
60
|
-
define_cached_method(owner,
|
68
|
+
define_cached_method(owner,
|
69
|
+
"global_#{attr_name}",
|
70
|
+
as: "sharded_global_#{attr_name}",
|
71
|
+
namespace: :switchman) do |batch|
|
61
72
|
batch << <<-RUBY
|
62
73
|
def sharded_global_#{attr_name}
|
63
74
|
raw_value = original_#{attr_name}
|
@@ -69,13 +80,16 @@ module Switchman
|
|
69
80
|
RUBY
|
70
81
|
end
|
71
82
|
else
|
72
|
-
define_method_unsharded_column(attr_name,
|
83
|
+
define_method_unsharded_column(attr_name, "global", owner)
|
73
84
|
end
|
74
85
|
end
|
75
86
|
|
76
87
|
def define_method_local_attribute(attr_name, owner:)
|
77
88
|
if sharded_column?(attr_name)
|
78
|
-
define_cached_method(owner,
|
89
|
+
define_cached_method(owner,
|
90
|
+
"local_#{attr_name}",
|
91
|
+
as: "sharded_local_#{attr_name}",
|
92
|
+
namespace: :switchman) do |batch|
|
79
93
|
batch << <<-RUBY
|
80
94
|
def sharded_local_#{attr_name}
|
81
95
|
raw_value = original_#{attr_name}
|
@@ -85,7 +99,7 @@ module Switchman
|
|
85
99
|
RUBY
|
86
100
|
end
|
87
101
|
else
|
88
|
-
define_method_unsharded_column(attr_name,
|
102
|
+
define_method_unsharded_column(attr_name, "local", owner)
|
89
103
|
end
|
90
104
|
end
|
91
105
|
|
@@ -97,7 +111,13 @@ module Switchman
|
|
97
111
|
if reflection.options[:polymorphic]
|
98
112
|
# a polymorphic association has to be discovered at runtime. This code ends up being something like
|
99
113
|
# context_type.&.constantize&.connection_class_for_self
|
100
|
-
|
114
|
+
<<~RUBY
|
115
|
+
begin
|
116
|
+
read_attribute(:#{reflection.foreign_type})&.constantize&.connection_class_for_self
|
117
|
+
rescue NameError
|
118
|
+
::ActiveRecord::Base
|
119
|
+
end
|
120
|
+
RUBY
|
101
121
|
else
|
102
122
|
# otherwise we can just return a symbol for the statically known type of the association
|
103
123
|
"::#{reflection.klass.connection_class_for_self.name}"
|
@@ -111,9 +131,14 @@ module Switchman
|
|
111
131
|
if sharded_column?(attr_name)
|
112
132
|
reflection = reflection_for_integer_attribute(attr_name)
|
113
133
|
class_name = connection_class_for_self_code_for_reflection(reflection)
|
114
|
-
safe_class_name = class_name.unpack1(
|
115
|
-
define_cached_method(owner,
|
116
|
-
|
134
|
+
safe_class_name = class_name.unpack1("h*")
|
135
|
+
define_cached_method(owner,
|
136
|
+
attr_name,
|
137
|
+
as: "sharded_#{safe_class_name}_#{attr_name}",
|
138
|
+
namespace: :switchman) do |batch|
|
139
|
+
batch << build_sharded_getter("sharded_#{safe_class_name}_#{attr_name}",
|
140
|
+
"original_#{attr_name}",
|
141
|
+
class_name)
|
117
142
|
end
|
118
143
|
else
|
119
144
|
define_cached_method(owner, attr_name, as: "plain_#{attr_name}", namespace: :switchman) do |batch|
|
@@ -153,8 +178,11 @@ module Switchman
|
|
153
178
|
if sharded_column?(attr_name)
|
154
179
|
reflection = reflection_for_integer_attribute(attr_name)
|
155
180
|
class_name = connection_class_for_self_code_for_reflection(reflection)
|
156
|
-
safe_class_name = class_name.unpack1(
|
157
|
-
define_cached_method(owner,
|
181
|
+
safe_class_name = class_name.unpack1("h*")
|
182
|
+
define_cached_method(owner,
|
183
|
+
"#{attr_name}=",
|
184
|
+
as: "sharded_#{safe_class_name}_#{attr_name}=",
|
185
|
+
namespace: :switchman) do |batch|
|
158
186
|
batch << build_sharded_setter("sharded_#{safe_class_name}_#{attr_name}", attr_name, class_name)
|
159
187
|
end
|
160
188
|
else
|
@@ -178,7 +206,10 @@ module Switchman
|
|
178
206
|
|
179
207
|
def define_method_original_attribute(attr_name, owner:)
|
180
208
|
if sharded_column?(attr_name)
|
181
|
-
define_cached_method(owner,
|
209
|
+
define_cached_method(owner,
|
210
|
+
"original_#{attr_name}",
|
211
|
+
as: "sharded_original_#{attr_name}",
|
212
|
+
namespace: :switchman) do |batch|
|
182
213
|
batch << <<-RUBY
|
183
214
|
def sharded_original_#{attr_name}
|
184
215
|
_read_attribute("#{attr_name}") { |n| missing_attribute(n, caller) }
|
@@ -186,14 +217,17 @@ module Switchman
|
|
186
217
|
RUBY
|
187
218
|
end
|
188
219
|
else
|
189
|
-
define_method_unsharded_column(attr_name,
|
220
|
+
define_method_unsharded_column(attr_name, "global", owner)
|
190
221
|
end
|
191
222
|
end
|
192
223
|
|
193
224
|
def define_method_original_attribute=(attr_name, owner:)
|
194
225
|
return unless sharded_column?(attr_name)
|
195
226
|
|
196
|
-
define_cached_method(owner,
|
227
|
+
define_cached_method(owner,
|
228
|
+
"original_#{attr_name}=",
|
229
|
+
as: "sharded_original_#{attr_name}=",
|
230
|
+
namespace: :switchman) do |batch|
|
197
231
|
batch << <<-RUBY
|
198
232
|
def sharded_original_#{attr_name}=(new_value)
|
199
233
|
_write_attribute('#{attr_name}', new_value)
|
@@ -203,9 +237,12 @@ module Switchman
|
|
203
237
|
end
|
204
238
|
|
205
239
|
def define_method_unsharded_column(attr_name, prefix, owner)
|
206
|
-
return if columns_hash["#{prefix}_#{attr_name}"] || attr_name ==
|
240
|
+
return if columns_hash["#{prefix}_#{attr_name}"] || attr_name == "id"
|
207
241
|
|
208
|
-
define_cached_method(owner,
|
242
|
+
define_cached_method(owner,
|
243
|
+
"#{prefix}_#{attr_name}",
|
244
|
+
as: "unsharded_#{prefix}_#{attr_name}",
|
245
|
+
namespace: :switchman) do |batch|
|
209
246
|
batch << <<-RUBY
|
210
247
|
def unsharded_#{prefix}_#{attr_name}
|
211
248
|
raise NoMethodError, "undefined method `#{prefix}_#{attr_name}'; are you missing an association?"
|
@@ -217,8 +254,8 @@ module Switchman
|
|
217
254
|
|
218
255
|
def self.prepended(klass)
|
219
256
|
klass.singleton_class.prepend(ClassMethods)
|
220
|
-
klass.attribute_method_prefix
|
221
|
-
klass.attribute_method_affix prefix:
|
257
|
+
klass.attribute_method_prefix "global_", "local_", "original_"
|
258
|
+
klass.attribute_method_affix prefix: "original_", suffix: "="
|
222
259
|
end
|
223
260
|
|
224
261
|
# these are called if the specific methods haven't been defined yet
|
@@ -226,7 +263,11 @@ module Switchman
|
|
226
263
|
return super unless self.class.sharded_column?(attr_name)
|
227
264
|
|
228
265
|
reflection = self.class.send(:reflection_for_integer_attribute, attr_name)
|
229
|
-
::Switchman::Shard.relative_id_for(
|
266
|
+
::Switchman::Shard.relative_id_for(
|
267
|
+
super,
|
268
|
+
shard,
|
269
|
+
::Switchman::Shard.current(connection_class_for_self_for_reflection(reflection))
|
270
|
+
)
|
230
271
|
end
|
231
272
|
|
232
273
|
def attribute=(attr_name, new_value)
|
@@ -236,7 +277,11 @@ module Switchman
|
|
236
277
|
end
|
237
278
|
|
238
279
|
reflection = self.class.send(:reflection_for_integer_attribute, attr_name)
|
239
|
-
super(
|
280
|
+
super(attr_name, ::Switchman::Shard.relative_id_for(
|
281
|
+
new_value,
|
282
|
+
::Switchman::Shard.current(connection_class_for_self_for_reflection(reflection)),
|
283
|
+
shard
|
284
|
+
))
|
240
285
|
end
|
241
286
|
|
242
287
|
def global_attribute(attr_name)
|