switchman 3.0.5 → 4.0.0

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +16 -15
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
  4. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +1 -1
  5. data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
  6. data/lib/switchman/action_controller/caching.rb +2 -2
  7. data/lib/switchman/active_record/abstract_adapter.rb +6 -15
  8. data/lib/switchman/active_record/associations.rb +331 -0
  9. data/lib/switchman/active_record/attribute_methods.rb +182 -77
  10. data/lib/switchman/active_record/base.rb +249 -46
  11. data/lib/switchman/active_record/calculations.rb +98 -44
  12. data/lib/switchman/active_record/connection_handler.rb +18 -0
  13. data/lib/switchman/active_record/connection_pool.rb +27 -28
  14. data/lib/switchman/active_record/database_configurations.rb +44 -6
  15. data/lib/switchman/active_record/finder_methods.rb +46 -16
  16. data/lib/switchman/active_record/log_subscriber.rb +11 -5
  17. data/lib/switchman/active_record/migration.rb +52 -5
  18. data/lib/switchman/active_record/model_schema.rb +1 -1
  19. data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
  20. data/lib/switchman/active_record/persistence.rb +37 -2
  21. data/lib/switchman/active_record/postgresql_adapter.rb +12 -11
  22. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  23. data/lib/switchman/active_record/query_cache.rb +49 -20
  24. data/lib/switchman/active_record/query_methods.rb +202 -136
  25. data/lib/switchman/active_record/reflection.rb +1 -1
  26. data/lib/switchman/active_record/relation.rb +40 -28
  27. data/lib/switchman/active_record/spawn_methods.rb +2 -2
  28. data/lib/switchman/active_record/statement_cache.rb +11 -7
  29. data/lib/switchman/active_record/table_definition.rb +1 -1
  30. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  31. data/lib/switchman/active_record/test_fixtures.rb +53 -0
  32. data/lib/switchman/active_support/cache.rb +25 -4
  33. data/lib/switchman/arel.rb +45 -7
  34. data/lib/switchman/call_super.rb +2 -2
  35. data/lib/switchman/database_server.rb +123 -79
  36. data/lib/switchman/default_shard.rb +14 -5
  37. data/lib/switchman/engine.rb +79 -131
  38. data/lib/switchman/environment.rb +2 -2
  39. data/lib/switchman/errors.rb +17 -2
  40. data/lib/switchman/guard_rail/relation.rb +7 -10
  41. data/lib/switchman/guard_rail.rb +5 -0
  42. data/lib/switchman/parallel.rb +68 -0
  43. data/lib/switchman/r_spec_helper.rb +17 -28
  44. data/lib/switchman/rails.rb +1 -4
  45. data/{app/models → lib}/switchman/shard.rb +226 -241
  46. data/lib/switchman/sharded_instrumenter.rb +3 -3
  47. data/lib/switchman/shared_schema_cache.rb +11 -0
  48. data/lib/switchman/standard_error.rb +15 -12
  49. data/lib/switchman/test_helper.rb +2 -2
  50. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  51. data/lib/switchman/version.rb +1 -1
  52. data/lib/switchman.rb +44 -12
  53. data/lib/tasks/switchman.rake +101 -54
  54. metadata +50 -58
  55. data/lib/switchman/active_record/association.rb +0 -206
  56. data/lib/switchman/open4.rb +0 -80
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3f9894955745b77bd418c8cede4882f5f37bf8cd3c0e2a3eecd53baa8658e1a
4
- data.tar.gz: d160939ff0c15d4c57b0836abe0850e9c8a499fc05eb410932dc038d2835c60f
3
+ metadata.gz: eb5bc75d858d97e55442dfdafcd1826d7040fe1bd4819bc3c2ee6053257a72ab
4
+ data.tar.gz: e73beaf3a5987f26e2402078f66c028831afc6924f21546d0ca1cb35f70e0831
5
5
  SHA512:
6
- metadata.gz: 89dc3d801b5f97d2147f10bcfb691279395590b45e9631103c73f66e487345e8608e185c07f5e54429bf43d29bd3f35c017bd93cd9354a88e29d0456d1d5040f
7
- data.tar.gz: 9893296f2801ded4b1e227a4672b0e474735d2c6ba4ff7f5e95d0591b753659a97066f422ca0615d8abbd4c1e5f9bd4c8250ff9c3c697fcb18640b3ff47dd219
6
+ metadata.gz: 273718d8bc773ded149ac5d81509a6d85c6858f17bc7c1a03f3dbdf15bbb96f101fea5842eddf87ba442aabf1f0bffe1a21421da4b9329548b0f40e51de26645
7
+ data.tar.gz: 9f81d1963fe213d334aed7377057fe29ecbdb637c6b2b8a33a49d52fcc0f726e1a8e87853a6e1030c8c2b0bbaff51e369d54cf8118f1c512ca0d62bf5684267c
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]
@@ -2,10 +2,10 @@
2
2
 
3
3
  class AddDefaultShardIndex < ActiveRecord::Migration[4.2]
4
4
  def change
5
- Switchman::Shard.where(default: nil).update_all(default: false)
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
  {}
@@ -4,7 +4,7 @@ class AddTimestampsToShards < ActiveRecord::Migration[4.2]
4
4
  disable_ddl_transaction!
5
5
 
6
6
  def change
7
- add_timestamps :switchman_shards, null: true
7
+ add_timestamps :switchman_shards, null: true, if_not_exists: true
8
8
  now = Time.now.utc
9
9
  Switchman::Shard.update_all(updated_at: now, created_at: now) if Switchman::Shard.current.default?
10
10
  change_column_null :switchman_shards, :updated_at, false
@@ -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,26 +27,19 @@ 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
42
42
  end
43
-
44
- private
45
-
46
- def id_value_for_database(value)
47
- return super unless value.class.sharded_primary_key?
48
-
49
- # do this the Rails 4.2 way, so that if Shard.current != self.shard, the id gets transposed
50
- quote(value.id)
51
- end
52
43
  end
53
44
  end
54
45
  end
@@ -0,0 +1,331 @@
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
+ raw_records ||= loader_query.records_for([self])
207
+
208
+ @preloaded_records = raw_records.select do |record|
209
+ assignments = false
210
+
211
+ owner_key = record[association_key_name]
212
+ if owner_key && record.class.sharded_column?(association_key_name)
213
+ owner_key = Shard.global_id_for(owner_key,
214
+ record.shard)
215
+ end
216
+
217
+ owners_by_key[convert_key(owner_key)]&.each do |owner|
218
+ entries = (@records_by_owner[owner] ||= [])
219
+
220
+ if reflection.collection? || entries.empty?
221
+ entries << record
222
+ assignments = true
223
+ end
224
+ end
225
+
226
+ assignments
227
+ end
228
+ end
229
+
230
+ # significant change: globalize keys on sharded columns
231
+ def owners_by_key
232
+ @owners_by_key ||= owners.each_with_object({}) do |owner, result|
233
+ key = owner[owner_key_name]
234
+ key = Shard.global_id_for(key, owner.shard) if key && owner.class.sharded_column?(owner_key_name)
235
+ key = convert_key(key)
236
+ (result[key] ||= []) << owner if key
237
+ end
238
+ end
239
+
240
+ # significant change: don't cache scope (since it could be for different shards)
241
+ def scope
242
+ build_scope
243
+ end
244
+ end
245
+ end
246
+
247
+ module CollectionProxy
248
+ def initialize(*args)
249
+ super
250
+ self.shard_value = scope.shard_value
251
+ self.shard_source_value = :association
252
+ end
253
+
254
+ def shard(*args)
255
+ scope.shard(*args)
256
+ end
257
+ end
258
+
259
+ module AutosaveAssociation
260
+ if ::Rails.version < "7.1"
261
+ def association_foreign_key_changed?(reflection, record, key)
262
+ return false if reflection.through_reflection?
263
+
264
+ # have to use send instead of _read_attribute because sharding
265
+ record.has_attribute?(reflection.foreign_key) && record.send(reflection.foreign_key) != key
266
+ end
267
+
268
+ def save_belongs_to_association(reflection)
269
+ # this seems counter-intuitive, but the autosave code will assign to attribute bypassing switchman,
270
+ # after reading the id attribute _without_ bypassing switchman. So we need Shard.current for the
271
+ # category of the associated record to match Shard.current for the category of self
272
+ shard.activate(connection_class_for_self_for_reflection(reflection)) { super }
273
+ end
274
+ else
275
+ def association_foreign_key_changed?(reflection, record, key)
276
+ return false if reflection.through_reflection?
277
+
278
+ foreign_key = Array(reflection.foreign_key)
279
+ return false unless foreign_key.all? { |k| record._has_attribute?(k) }
280
+
281
+ # have to use send instead of _read_attribute because sharding
282
+ foreign_key.map { |k| record.send(k) } != Array(key)
283
+ end
284
+
285
+ def save_belongs_to_association(reflection)
286
+ association = association_instance_get(reflection.name)
287
+ return unless association&.loaded? && !association.stale_target?
288
+
289
+ record = association.load_target
290
+ return unless record && !record.destroyed?
291
+
292
+ autosave = reflection.options[:autosave]
293
+
294
+ if autosave && record.marked_for_destruction?
295
+ foreign_key = Array(reflection.foreign_key)
296
+ foreign_key.each { |key| self[key] = nil }
297
+ record.destroy
298
+ elsif autosave != false
299
+ if record.new_record? || (autosave && record.changed_for_autosave?)
300
+ saved = record.save(validate: !autosave)
301
+ end
302
+
303
+ if association.updated?
304
+ primary_key = Array(compute_primary_key(reflection, record)).map(&:to_s)
305
+ foreign_key = Array(reflection.foreign_key)
306
+
307
+ primary_key_foreign_key_pairs = primary_key.zip(foreign_key)
308
+ primary_key_foreign_key_pairs.each do |pk, fk|
309
+ # Notable change: add relative_id_for here
310
+ association_id = if record.class.sharded_column?(pk)
311
+ Shard.relative_id_for(
312
+ record._read_attribute(pk),
313
+ record.shard,
314
+ shard
315
+ )
316
+ else
317
+ record._read_attribute(pk)
318
+ end
319
+ self[fk] = association_id unless self[fk] == association_id
320
+ end
321
+ association.loaded!
322
+ end
323
+
324
+ saved if autosave
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end
330
+ end
331
+ end