switchman 3.0.14 → 3.5.20

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +16 -15
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
  4. data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
  5. data/lib/switchman/action_controller/caching.rb +2 -2
  6. data/lib/switchman/active_record/abstract_adapter.rb +6 -6
  7. data/lib/switchman/active_record/associations.rb +365 -0
  8. data/lib/switchman/active_record/attribute_methods.rb +188 -99
  9. data/lib/switchman/active_record/base.rb +185 -40
  10. data/lib/switchman/active_record/calculations.rb +64 -40
  11. data/lib/switchman/active_record/connection_handler.rb +18 -0
  12. data/lib/switchman/active_record/connection_pool.rb +24 -5
  13. data/lib/switchman/active_record/database_configurations.rb +37 -13
  14. data/lib/switchman/active_record/finder_methods.rb +46 -16
  15. data/lib/switchman/active_record/log_subscriber.rb +11 -5
  16. data/lib/switchman/active_record/migration.rb +52 -8
  17. data/lib/switchman/active_record/model_schema.rb +1 -1
  18. data/lib/switchman/active_record/persistence.rb +31 -3
  19. data/lib/switchman/active_record/postgresql_adapter.rb +11 -10
  20. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  21. data/lib/switchman/active_record/query_cache.rb +49 -20
  22. data/lib/switchman/active_record/query_methods.rb +187 -136
  23. data/lib/switchman/active_record/reflection.rb +1 -1
  24. data/lib/switchman/active_record/relation.rb +33 -26
  25. data/lib/switchman/active_record/spawn_methods.rb +2 -2
  26. data/lib/switchman/active_record/statement_cache.rb +11 -7
  27. data/lib/switchman/active_record/table_definition.rb +1 -1
  28. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  29. data/lib/switchman/active_record/test_fixtures.rb +26 -16
  30. data/lib/switchman/active_support/cache.rb +20 -1
  31. data/lib/switchman/arel.rb +34 -18
  32. data/lib/switchman/call_super.rb +8 -2
  33. data/lib/switchman/database_server.rb +91 -45
  34. data/lib/switchman/default_shard.rb +14 -5
  35. data/lib/switchman/engine.rb +79 -126
  36. data/lib/switchman/environment.rb +2 -2
  37. data/lib/switchman/errors.rb +17 -2
  38. data/lib/switchman/guard_rail/relation.rb +8 -10
  39. data/lib/switchman/guard_rail.rb +5 -0
  40. data/lib/switchman/parallel.rb +68 -0
  41. data/lib/switchman/r_spec_helper.rb +14 -11
  42. data/lib/switchman/rails.rb +2 -5
  43. data/{app/models → lib}/switchman/shard.rb +186 -189
  44. data/lib/switchman/sharded_instrumenter.rb +5 -1
  45. data/lib/switchman/shared_schema_cache.rb +11 -0
  46. data/lib/switchman/standard_error.rb +6 -5
  47. data/lib/switchman/test_helper.rb +2 -2
  48. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  49. data/lib/switchman/version.rb +1 -1
  50. data/lib/switchman.rb +44 -12
  51. data/lib/tasks/switchman.rake +74 -53
  52. metadata +42 -53
  53. data/lib/switchman/active_record/association.rb +0 -206
  54. data/lib/switchman/open4.rb +0 -80
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 148d79c6d4c79ef51204c2d32718b30463814b56849effaaf9cacaf86789261d
4
- data.tar.gz: 1db4039008bd8958e0dc918b0de81a85294f795f6aea23a1e68e592d6ddc691f
3
+ metadata.gz: 88d7a6f4e778e781d7706281f5330b378a7f7982fe7475a95b76693c1549d146
4
+ data.tar.gz: e8ebfc94e5a57a56a5a44f2924d3f273fd5424211ead5def892c19d5fca0ba7b
5
5
  SHA512:
6
- metadata.gz: c2ff7f714441d360a38125d7eea85ac3aba981d76fa8a8fceca527f499b7c1a90eb4dd2c91fcf6e292569089e35b077b7ad248cb069a3a9a0579071b93f54a0d
7
- data.tar.gz: 7fc113c8b0a1ecdd6d5590401af399fc8f7dd220bc655c190e962870f5e64e2d1e922879e3515693713a9cc77b290913d38740102a29000c19071dab21c10844
6
+ metadata.gz: 983b75007895318e8e1dda6022f755c923e3b6d390ce9112037eb202be9384caaa7f7fd9e633c1e200f75bbd5a2346f9871cd60aed71f1b7b7b5a04f6aa63906
7
+ data.tar.gz: c91f50e916dc069b75fc8260c1ae52cd64e132e8eb68bb22c182a4291b87bf9a03bea390c8797b50b7b477fc7c82af8670b99e0213e4c5b712e097295912d98c
data/Rakefile CHANGED
@@ -2,37 +2,38 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  begin
5
- require 'bundler/setup'
5
+ require "bundler/setup"
6
6
  rescue LoadError
7
- puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
8
8
  end
9
9
  begin
10
- require 'rdoc/task'
10
+ require "rdoc/task"
11
11
  rescue LoadError
12
- require 'rdoc/rdoc'
13
- require 'rake/rdoctask'
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 = 'rdoc'
19
- rdoc.title = 'Switchman'
20
- rdoc.options << '--line-numbers'
21
- rdoc.rdoc_files.include('lib/**/*.rb')
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
- APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
25
- load 'rails/tasks/engine.rake'
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 'rspec/core/rake_task'
30
+ require "rspec/core/rake_task"
30
31
  RSpec::Core::RakeTask.new
31
32
 
32
- require 'rubocop/rake_task'
33
+ require "rubocop/rake_task"
33
34
 
34
35
  RuboCop::RakeTask.new do |task|
35
- task.options = ['-S']
36
+ task.options = ["-S"]
36
37
  end
37
38
 
38
- task default: %i[spec rubocop]
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 == 'PostgreSQL'
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, :database_server_id, unique: true, where: 'name IS NULL',
7
- name: 'index_switchman_shards_unique_primary_shard'
8
- add_index :switchman_shards, '(true)', unique: true, where: 'database_server_id IS NULL AND name IS NULL',
9
- name: 'index_switchman_shards_unique_primary_db_and_shard'
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
@@ -13,8 +13,8 @@ module Switchman
13
13
  # disallow assigning to ActionController::Base.cache_store or
14
14
  # ActionController::Base#cache_store for the same reasons we disallow
15
15
  # assigning to Rails.cache
16
- def cache_store=(_cache)
17
- raise NoMethodError
16
+ def cache_store=(cache)
17
+ raise NoMethodError unless cache == ::Rails.cache
18
18
  end
19
19
  end
20
20
 
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'switchman/sharded_instrumenter'
4
-
5
3
  module Switchman
6
4
  module ActiveRecord
7
5
  module AbstractAdapter
8
6
  module ForeignKeyCheck
9
7
  def add_column(table, name, type, limit: nil, **)
10
- Engine.foreign_key_check(name, type, limit: limit)
8
+ Switchman.foreign_key_check(name, type, limit: limit)
11
9
  super
12
10
  end
13
11
  end
@@ -29,13 +27,15 @@ module Switchman
29
27
  quote_table_name(name)
30
28
  end
31
29
 
32
- def schema_migration
33
- ::ActiveRecord::SchemaMigration
30
+ if ::Rails.version < "7.1"
31
+ def schema_migration
32
+ ::ActiveRecord::SchemaMigration
33
+ end
34
34
  end
35
35
 
36
36
  protected
37
37
 
38
- def log(*args, &block)
38
+ def log(...)
39
39
  super
40
40
  ensure
41
41
  @last_query_at = Time.now
@@ -0,0 +1,365 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ module ActiveRecord
5
+ module Associations
6
+ module Association
7
+ def shard
8
+ reflection.shard(owner)
9
+ end
10
+
11
+ def build_record(*args)
12
+ shard.activate { super }
13
+ end
14
+
15
+ def load_target
16
+ shard.activate { super }
17
+ end
18
+
19
+ def scope
20
+ shard_value = @reflection.options[:multishard] ? @owner : shard
21
+ @owner.shard.activate { super.shard(shard_value, :association) }
22
+ end
23
+ end
24
+
25
+ module CollectionAssociation
26
+ def find_target
27
+ shards = if reflection.options[:multishard] && owner.respond_to?(:associated_shards)
28
+ owner.associated_shards
29
+ else
30
+ [shard]
31
+ end
32
+ # activate both the owner and the target's shard category, so that Reflection#join_id_for,
33
+ # when called for the owner, will be returned relative to shard the query will execute on
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
44
+ end
45
+ end
46
+
47
+ def _create_record(*)
48
+ shard.activate { super }
49
+ end
50
+ end
51
+
52
+ module BelongsToAssociation
53
+ def replace_keys(record, force: false)
54
+ if record&.class&.sharded_column?(reflection.association_primary_key(record.class))
55
+ foreign_id = record[reflection.association_primary_key(record.class)]
56
+ owner[reflection.foreign_key] = Shard.relative_id_for(foreign_id, record.shard, owner.shard)
57
+ else
58
+ super
59
+ end
60
+ end
61
+
62
+ def shard
63
+ if @owner.class.sharded_column?(@reflection.foreign_key) &&
64
+ (foreign_id = @owner[@reflection.foreign_key])
65
+ Shard.shard_for(foreign_id, @owner.loaded_from_shard)
66
+ else
67
+ super
68
+ end
69
+ end
70
+ end
71
+
72
+ module ForeignAssociation
73
+ # significant change:
74
+ # * transpose the key to the correct shard
75
+ def set_owner_attributes(record) # rubocop:disable Naming/AccessorMethodName
76
+ return if options[:through]
77
+
78
+ key = owner._read_attribute(reflection.join_foreign_key)
79
+ key = Shard.relative_id_for(key, owner.shard, shard)
80
+ record._write_attribute(reflection.join_primary_key, key)
81
+
82
+ record._write_attribute(reflection.type, owner.class.polymorphic_name) if reflection.type
83
+ end
84
+ end
85
+
86
+ module Extension
87
+ def self.build(_model, _reflection); end
88
+
89
+ def self.valid_options
90
+ [:multishard]
91
+ end
92
+ end
93
+
94
+ ::ActiveRecord::Associations::Builder::Association.extensions << Extension
95
+
96
+ module Preloader
97
+ module Association
98
+ # significant changes:
99
+ # * associate shards with records
100
+ # * look on all appropriate shards when loading records
101
+ module LoaderRecords
102
+ def populate_keys_to_load_and_already_loaded_records
103
+ @sharded_keys_to_load = {}
104
+
105
+ loaders.each do |loader|
106
+ multishard = loader.send(:reflection).options[:multishard]
107
+ belongs_to = loader.send(:reflection).macro == :belongs_to
108
+ loader.owners_by_key.each do |key, owners|
109
+ if (loaded_owner = owners.find { |owner| loader.loaded?(owner) })
110
+ already_loaded_records_by_key[key] = loader.target_for(loaded_owner)
111
+ else
112
+ shard_set = @sharded_keys_to_load[key] ||= Set.new
113
+ owner_key_name = loader.send(:owner_key_name)
114
+ owners.each do |owner|
115
+ if multishard && owner.respond_to?(:associated_shards)
116
+ shard_set.merge(owner.associated_shards.map(&:id))
117
+ elsif belongs_to && owner.class.sharded_column?(owner_key_name)
118
+ shard_set.add(Shard.shard_for(owner[owner_key_name], owner.shard).id)
119
+ elsif belongs_to
120
+ shard_set.add(Shard.current.id)
121
+ else
122
+ shard_set.add(owner.shard.id)
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ @sharded_keys_to_load.delete_if { |key, _shards| already_loaded_records_by_key.include?(key) }
130
+ end
131
+
132
+ def load_records
133
+ ret = []
134
+
135
+ shards_with_keys = @sharded_keys_to_load.each_with_object({}) do |(key, shards), h|
136
+ shards.each { |shard| (h[shard] ||= []) << key }
137
+ end
138
+
139
+ shards_with_keys.each do |shard, keys|
140
+ Shard.lookup(shard).activate do
141
+ scope_was = loader_query.scope
142
+ begin
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
+ )
149
+ ret += loader_query.load_records_for_keys(keys) do |record|
150
+ loaders.each { |l| l.set_inverse(record) }
151
+ end
152
+ ensure
153
+ loader_query.instance_variable_set(:@scope, scope_was)
154
+ end
155
+ end
156
+ end
157
+
158
+ ret
159
+ end
160
+ end
161
+
162
+ # Copypasta from Activerecord but with added global_id_for goodness.
163
+ def records_for(ids)
164
+ scope.where(association_key_name => ids).load do |record|
165
+ global_key = if model.connection_class_for_self == UnshardedRecord
166
+ convert_key(record[association_key_name])
167
+ else
168
+ Shard.global_id_for(record[association_key_name], record.shard)
169
+ end
170
+ owner = owners_by_key[convert_key(global_key)].first
171
+ association = owner.association(reflection.name)
172
+ association.set_inverse_instance(record)
173
+ end
174
+ end
175
+
176
+ # Disabling to keep closer to rails original
177
+ # rubocop:disable Naming/AccessorMethodName, Style/GuardClause
178
+ # significant changes:
179
+ # * globalize the key to lookup
180
+ def set_inverse(record)
181
+ global_key = if model.connection_class_for_self == UnshardedRecord
182
+ convert_key(record[association_key_name])
183
+ else
184
+ Shard.global_id_for(record[association_key_name], record.shard)
185
+ end
186
+
187
+ if (owners = owners_by_key[convert_key(global_key)])
188
+ # Processing only the first owner
189
+ # because the record is modified but not an owner
190
+ association = owners.first.association(reflection.name)
191
+ association.set_inverse_instance(record)
192
+ end
193
+ end
194
+ # rubocop:enable Naming/AccessorMethodName, Style/GuardClause
195
+
196
+ # significant changes:
197
+ # * partition_by_shard the records_for call
198
+ # * re-globalize the fetched owner id before looking up in the map
199
+ # TODO: the ignored param currently loads records; we should probably not waste effort double-loading them
200
+ # Change introduced here: https://github.com/rails/rails/commit/c6c0b2e8af64509b699b782aadfecaa430700ece
201
+ def load_records(raw_records = nil)
202
+ # owners can be duplicated when a relation has a collection association join
203
+ # #compare_by_identity makes such owners different hash keys
204
+ @records_by_owner = {}.compare_by_identity
205
+
206
+ if ::Rails.version >= "7.0"
207
+ raw_records ||= loader_query.records_for([self])
208
+ elsif owner_keys.empty?
209
+ raw_records ||= []
210
+ else
211
+ # determine the shard to search for each owner
212
+ if reflection.macro == :belongs_to
213
+ # for belongs_to, it's the shard of the foreign_key
214
+ partition_proc = lambda do |owner|
215
+ if owner.class.sharded_column?(owner_key_name)
216
+ Shard.shard_for(owner[owner_key_name], owner.shard)
217
+ else
218
+ Shard.current
219
+ end
220
+ end
221
+ elsif !reflection.options[:multishard]
222
+ # for non-multishard associations, it's *just* the owner's shard
223
+ partition_proc = ->(owner) { owner.shard }
224
+ end
225
+
226
+ raw_records ||= Shard.partition_by_shard(owners, partition_proc) do |partitioned_owners|
227
+ relative_owner_keys = partitioned_owners.map do |owner|
228
+ key = owner[owner_key_name]
229
+ if key && owner.class.sharded_column?(owner_key_name)
230
+ key = Shard.relative_id_for(key,
231
+ owner.shard,
232
+ Shard.current(klass.connection_class_for_self))
233
+ end
234
+ convert_key(key)
235
+ end
236
+ relative_owner_keys.compact!
237
+ relative_owner_keys.uniq!
238
+ records_for(relative_owner_keys)
239
+ end
240
+ end
241
+
242
+ @preloaded_records = raw_records.select do |record|
243
+ assignments = false
244
+
245
+ owner_key = record[association_key_name]
246
+ if owner_key && record.class.sharded_column?(association_key_name)
247
+ owner_key = Shard.global_id_for(owner_key,
248
+ record.shard)
249
+ end
250
+
251
+ owners_by_key[convert_key(owner_key)]&.each do |owner|
252
+ entries = (@records_by_owner[owner] ||= [])
253
+
254
+ if reflection.collection? || entries.empty?
255
+ entries << record
256
+ assignments = true
257
+ end
258
+ end
259
+
260
+ assignments
261
+ end
262
+ end
263
+
264
+ # significant change: globalize keys on sharded columns
265
+ def owners_by_key
266
+ @owners_by_key ||= owners.each_with_object({}) do |owner, result|
267
+ key = owner[owner_key_name]
268
+ key = Shard.global_id_for(key, owner.shard) if key && owner.class.sharded_column?(owner_key_name)
269
+ key = convert_key(key)
270
+ (result[key] ||= []) << owner if key
271
+ end
272
+ end
273
+
274
+ # significant change: don't cache scope (since it could be for different shards)
275
+ def scope
276
+ build_scope
277
+ end
278
+ end
279
+ end
280
+
281
+ module CollectionProxy
282
+ def initialize(*args)
283
+ super
284
+ self.shard_value = scope.shard_value
285
+ self.shard_source_value = :association
286
+ end
287
+
288
+ def shard(*args)
289
+ scope.shard(*args)
290
+ end
291
+ end
292
+
293
+ module AutosaveAssociation
294
+ if ::Rails.version < "7.1"
295
+ def association_foreign_key_changed?(reflection, record, key)
296
+ return false if reflection.through_reflection?
297
+
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
318
+
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
361
+ end
362
+ end
363
+ end
364
+ end
365
+ end