switchman 3.0.24 → 4.2.4
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 +16 -15
- 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 +11 -7
- data/lib/switchman/active_record/associations.rb +157 -50
- data/lib/switchman/active_record/attribute_methods.rb +192 -101
- data/lib/switchman/active_record/base.rb +136 -33
- data/lib/switchman/active_record/calculations.rb +91 -48
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +41 -6
- data/lib/switchman/active_record/database_configurations.rb +23 -13
- data/lib/switchman/active_record/finder_methods.rb +22 -16
- data/lib/switchman/active_record/log_subscriber.rb +3 -6
- data/lib/switchman/active_record/migration.rb +42 -17
- data/lib/switchman/active_record/model_schema.rb +1 -1
- data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
- data/lib/switchman/active_record/persistence.rb +32 -2
- data/lib/switchman/active_record/postgresql_adapter.rb +37 -22
- data/lib/switchman/active_record/predicate_builder.rb +2 -2
- data/lib/switchman/active_record/query_cache.rb +26 -17
- data/lib/switchman/active_record/query_methods.rb +249 -142
- data/lib/switchman/active_record/reflection.rb +10 -3
- data/lib/switchman/active_record/relation.rb +103 -32
- data/lib/switchman/active_record/spawn_methods.rb +3 -7
- data/lib/switchman/active_record/statement_cache.rb +13 -9
- data/lib/switchman/active_record/table_definition.rb +1 -1
- data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
- data/lib/switchman/active_record/test_fixtures.rb +71 -25
- data/lib/switchman/active_support/cache.rb +9 -4
- data/lib/switchman/arel.rb +16 -25
- data/lib/switchman/call_super.rb +2 -2
- data/lib/switchman/database_server.rb +68 -34
- data/lib/switchman/default_shard.rb +14 -3
- data/lib/switchman/engine.rb +36 -19
- data/lib/switchman/environment.rb +2 -2
- data/lib/switchman/errors.rb +13 -0
- data/lib/switchman/guard_rail/relation.rb +3 -3
- data/lib/switchman/parallel.rb +6 -6
- data/lib/switchman/r_spec_helper.rb +12 -11
- data/lib/switchman/shard.rb +182 -72
- data/lib/switchman/sharded_instrumenter.rb +9 -3
- data/lib/switchman/shared_schema_cache.rb +11 -0
- data/lib/switchman/standard_error.rb +4 -0
- data/lib/switchman/test_helper.rb +3 -3
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +27 -15
- data/lib/tasks/switchman.rake +96 -60
- metadata +18 -168
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b26ae7fe0a43054e224f7104377e1a47d8cb56e4a5e05cb443f76045901f8298
|
|
4
|
+
data.tar.gz: 220c7665740e8ff3cf7a27b0bed847e744bd2d0732c35d93ecd05489b71ccd7c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2dc2d565571d81b9e222c3b083095a6b11ff2c22b4f756ed0e2e03d859e44d1d3f3e1696a3510ee5fa3e97b4595a2a5def19e70f0a5bd28d547fc6c277ae4631
|
|
7
|
+
data.tar.gz: 9780190480638be3fedc7b15bd6c34a5e3de8478051e18835684aec755752a894236145f9ba5061fafc483fa3b500d2da51ce4fb95748a809d3c506620cb8167
|
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
|
-
task default: %i[spec
|
|
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
|
|
@@ -5,7 +5,7 @@ module Switchman
|
|
|
5
5
|
module AbstractAdapter
|
|
6
6
|
module ForeignKeyCheck
|
|
7
7
|
def add_column(table, name, type, limit: nil, **)
|
|
8
|
-
Switchman.foreign_key_check(name, type, limit:
|
|
8
|
+
Switchman.foreign_key_check(name, type, limit:)
|
|
9
9
|
super
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -19,21 +19,25 @@ module Switchman
|
|
|
19
19
|
|
|
20
20
|
def initialize(*args)
|
|
21
21
|
super
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
@instrumenter = Switchman::ShardedInstrumenter.new(@instrumenter, self) if ::Rails.version < "8.0"
|
|
24
|
+
|
|
23
25
|
@last_query_at = Time.now
|
|
24
26
|
end
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
if ::Rails.version >= "8.0"
|
|
29
|
+
def instrumenter # :nodoc:
|
|
30
|
+
@instrumenter ||= Switchman::ShardedInstrumenter.new(::ActiveSupport::Notifications.instrumenter, self)
|
|
31
|
+
end
|
|
28
32
|
end
|
|
29
33
|
|
|
30
|
-
def
|
|
31
|
-
|
|
34
|
+
def quote_local_table_name(name)
|
|
35
|
+
quote_table_name(name)
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
protected
|
|
35
39
|
|
|
36
|
-
def log(
|
|
40
|
+
def log(...)
|
|
37
41
|
super
|
|
38
42
|
ensure
|
|
39
43
|
@last_query_at = Time.now
|
|
@@ -23,12 +23,26 @@ module Switchman
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
module CollectionAssociation
|
|
26
|
-
def find_target
|
|
27
|
-
shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards)
|
|
26
|
+
def find_target(async: false)
|
|
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
|
+
elsif ::Rails.version < "8.0"
|
|
42
|
+
super()
|
|
43
|
+
else
|
|
44
|
+
super
|
|
45
|
+
end
|
|
32
46
|
end
|
|
33
47
|
end
|
|
34
48
|
|
|
@@ -50,7 +64,7 @@ module Switchman
|
|
|
50
64
|
def shard
|
|
51
65
|
if @owner.class.sharded_column?(@reflection.foreign_key) &&
|
|
52
66
|
(foreign_id = @owner[@reflection.foreign_key])
|
|
53
|
-
Shard.shard_for(foreign_id, @owner.
|
|
67
|
+
Shard.shard_for(foreign_id, @owner.loaded_from_shard)
|
|
54
68
|
else
|
|
55
69
|
super
|
|
56
70
|
end
|
|
@@ -83,10 +97,74 @@ module Switchman
|
|
|
83
97
|
|
|
84
98
|
module Preloader
|
|
85
99
|
module Association
|
|
100
|
+
# significant changes:
|
|
101
|
+
# * associate shards with records
|
|
102
|
+
# * look on all appropriate shards when loading records
|
|
103
|
+
module LoaderRecords
|
|
104
|
+
def populate_keys_to_load_and_already_loaded_records
|
|
105
|
+
@sharded_keys_to_load = {}
|
|
106
|
+
|
|
107
|
+
loaders.each do |loader|
|
|
108
|
+
multishard = loader.send(:reflection).options[:multishard]
|
|
109
|
+
belongs_to = loader.send(:reflection).macro == :belongs_to
|
|
110
|
+
loader.owners_by_key.each do |key, owners|
|
|
111
|
+
if (loaded_owner = owners.find { |owner| loader.loaded?(owner) })
|
|
112
|
+
already_loaded_records_by_key[key] = loader.target_for(loaded_owner)
|
|
113
|
+
else
|
|
114
|
+
shard_set = @sharded_keys_to_load[key] ||= Set.new
|
|
115
|
+
owner_key_name = loader.send(:owner_key_name)
|
|
116
|
+
owners.each do |owner|
|
|
117
|
+
if multishard && owner.respond_to?(:associated_shards)
|
|
118
|
+
shard_set.merge(owner.associated_shards.map(&:id))
|
|
119
|
+
elsif belongs_to && owner.class.sharded_column?(owner_key_name)
|
|
120
|
+
shard_set.add(Shard.shard_for(owner[owner_key_name], owner.shard).id)
|
|
121
|
+
elsif belongs_to
|
|
122
|
+
shard_set.add(Shard.current.id)
|
|
123
|
+
else
|
|
124
|
+
shard_set.add(owner.shard.id)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
@sharded_keys_to_load.delete_if { |key, _shards| already_loaded_records_by_key.include?(key) }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def load_records
|
|
135
|
+
ret = []
|
|
136
|
+
|
|
137
|
+
shards_with_keys = @sharded_keys_to_load.each_with_object({}) do |(key, shards), h|
|
|
138
|
+
shards.each { |shard| (h[shard] ||= []) << key }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
shards_with_keys.each do |shard, keys|
|
|
142
|
+
Shard.lookup(shard).activate do
|
|
143
|
+
scope_was = loader_query.scope
|
|
144
|
+
begin
|
|
145
|
+
loader_query.instance_variable_set(
|
|
146
|
+
:@scope,
|
|
147
|
+
loader_query.scope.shard(
|
|
148
|
+
Shard.current(loader_query.scope.model.connection_class_for_self)
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
ret += loader_query.load_records_for_keys(keys) do |record|
|
|
152
|
+
loaders.each { |l| l.set_inverse(record) }
|
|
153
|
+
end
|
|
154
|
+
ensure
|
|
155
|
+
loader_query.instance_variable_set(:@scope, scope_was)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
ret
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
86
164
|
# Copypasta from Activerecord but with added global_id_for goodness.
|
|
87
165
|
def records_for(ids)
|
|
88
166
|
scope.where(association_key_name => ids).load do |record|
|
|
89
|
-
global_key = if model.
|
|
167
|
+
global_key = if model.connection_class_for_self == UnshardedRecord
|
|
90
168
|
convert_key(record[association_key_name])
|
|
91
169
|
else
|
|
92
170
|
Shard.global_id_for(record[association_key_name], record.shard)
|
|
@@ -97,46 +175,37 @@ module Switchman
|
|
|
97
175
|
end
|
|
98
176
|
end
|
|
99
177
|
|
|
178
|
+
# Disabling to keep closer to rails original
|
|
179
|
+
# rubocop:disable Naming/AccessorMethodName
|
|
180
|
+
# significant changes:
|
|
181
|
+
# * globalize the key to lookup
|
|
182
|
+
def set_inverse(record)
|
|
183
|
+
global_key = if model.connection_class_for_self == UnshardedRecord
|
|
184
|
+
convert_key(record[association_key_name])
|
|
185
|
+
else
|
|
186
|
+
Shard.global_id_for(record[association_key_name], record.shard)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if (owners = owners_by_key[convert_key(global_key)])
|
|
190
|
+
# Processing only the first owner
|
|
191
|
+
# because the record is modified but not an owner
|
|
192
|
+
association = owners.first.association(reflection.name)
|
|
193
|
+
association.set_inverse_instance(record)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
# rubocop:enable Naming/AccessorMethodName
|
|
197
|
+
|
|
100
198
|
# significant changes:
|
|
101
199
|
# * partition_by_shard the records_for call
|
|
102
200
|
# * re-globalize the fetched owner id before looking up in the map
|
|
103
|
-
|
|
201
|
+
# TODO: the ignored param currently loads records; we should probably not waste effort double-loading them
|
|
202
|
+
# Change introduced here: https://github.com/rails/rails/commit/c6c0b2e8af64509b699b782aadfecaa430700ece
|
|
203
|
+
def load_records(raw_records = nil)
|
|
104
204
|
# owners can be duplicated when a relation has a collection association join
|
|
105
205
|
# #compare_by_identity makes such owners different hash keys
|
|
106
206
|
@records_by_owner = {}.compare_by_identity
|
|
107
207
|
|
|
108
|
-
|
|
109
|
-
raw_records = []
|
|
110
|
-
else
|
|
111
|
-
# determine the shard to search for each owner
|
|
112
|
-
if reflection.macro == :belongs_to
|
|
113
|
-
# for belongs_to, it's the shard of the foreign_key
|
|
114
|
-
partition_proc = lambda do |owner|
|
|
115
|
-
if owner.class.sharded_column?(owner_key_name)
|
|
116
|
-
Shard.shard_for(owner[owner_key_name], owner.shard)
|
|
117
|
-
else
|
|
118
|
-
Shard.current
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
elsif !reflection.options[:multishard]
|
|
122
|
-
# for non-multishard associations, it's *just* the owner's shard
|
|
123
|
-
partition_proc = ->(owner) { owner.shard }
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
raw_records = Shard.partition_by_shard(owners, partition_proc) do |partitioned_owners|
|
|
127
|
-
relative_owner_keys = partitioned_owners.map do |owner|
|
|
128
|
-
key = owner[owner_key_name]
|
|
129
|
-
if key && owner.class.sharded_column?(owner_key_name)
|
|
130
|
-
key = Shard.relative_id_for(key, owner.shard,
|
|
131
|
-
Shard.current(klass.connection_classes))
|
|
132
|
-
end
|
|
133
|
-
convert_key(key)
|
|
134
|
-
end
|
|
135
|
-
relative_owner_keys.compact!
|
|
136
|
-
relative_owner_keys.uniq!
|
|
137
|
-
records_for(relative_owner_keys)
|
|
138
|
-
end
|
|
139
|
-
end
|
|
208
|
+
raw_records ||= loader_query.records_for([self])
|
|
140
209
|
|
|
141
210
|
@preloaded_records = raw_records.select do |record|
|
|
142
211
|
assignments = false
|
|
@@ -147,7 +216,7 @@ module Switchman
|
|
|
147
216
|
record.shard)
|
|
148
217
|
end
|
|
149
218
|
|
|
150
|
-
owners_by_key[convert_key(owner_key)]
|
|
219
|
+
owners_by_key[convert_key(owner_key)]&.each do |owner|
|
|
151
220
|
entries = (@records_by_owner[owner] ||= [])
|
|
152
221
|
|
|
153
222
|
if reflection.collection? || entries.empty?
|
|
@@ -184,23 +253,61 @@ module Switchman
|
|
|
184
253
|
self.shard_source_value = :association
|
|
185
254
|
end
|
|
186
255
|
|
|
187
|
-
def shard(*
|
|
188
|
-
scope.shard(*
|
|
256
|
+
def shard(*)
|
|
257
|
+
scope.shard(*)
|
|
189
258
|
end
|
|
190
259
|
end
|
|
191
260
|
|
|
192
261
|
module AutosaveAssociation
|
|
193
|
-
def
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
262
|
+
def association_foreign_key_changed?(reflection, record, key)
|
|
263
|
+
return false if reflection.through_reflection?
|
|
264
|
+
|
|
265
|
+
foreign_key = Array(reflection.foreign_key)
|
|
266
|
+
return false unless foreign_key.all? { |k| record._has_attribute?(k) }
|
|
267
|
+
|
|
268
|
+
# have to use send instead of _read_attribute because sharding
|
|
269
|
+
foreign_key.map { |k| record.send(k) } != Array(key)
|
|
197
270
|
end
|
|
198
271
|
|
|
199
272
|
def save_belongs_to_association(reflection)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
273
|
+
association = association_instance_get(reflection.name)
|
|
274
|
+
return unless association&.loaded? && !association.stale_target?
|
|
275
|
+
|
|
276
|
+
record = association.load_target
|
|
277
|
+
return unless record && !record.destroyed?
|
|
278
|
+
|
|
279
|
+
autosave = reflection.options[:autosave]
|
|
280
|
+
|
|
281
|
+
if autosave && record.marked_for_destruction?
|
|
282
|
+
foreign_key = Array(reflection.foreign_key)
|
|
283
|
+
foreign_key.each { |key| self[key] = nil }
|
|
284
|
+
record.destroy
|
|
285
|
+
elsif autosave != false
|
|
286
|
+
saved = record.save(validate: !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
|
|
287
|
+
|
|
288
|
+
if association.updated?
|
|
289
|
+
primary_key = Array(compute_primary_key(reflection, record)).map(&:to_s)
|
|
290
|
+
foreign_key = Array(reflection.foreign_key)
|
|
291
|
+
|
|
292
|
+
primary_key_foreign_key_pairs = primary_key.zip(foreign_key)
|
|
293
|
+
primary_key_foreign_key_pairs.each do |pk, fk|
|
|
294
|
+
# Notable change: add relative_id_for here
|
|
295
|
+
association_id = if record.class.sharded_column?(pk)
|
|
296
|
+
Shard.relative_id_for(
|
|
297
|
+
record._read_attribute(pk),
|
|
298
|
+
record.shard,
|
|
299
|
+
shard
|
|
300
|
+
)
|
|
301
|
+
else
|
|
302
|
+
record._read_attribute(pk)
|
|
303
|
+
end
|
|
304
|
+
self[fk] = association_id unless self[fk] == association_id
|
|
305
|
+
end
|
|
306
|
+
association.loaded!
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
saved if autosave
|
|
310
|
+
end
|
|
204
311
|
end
|
|
205
312
|
end
|
|
206
313
|
end
|