switchman 3.0.2 → 4.2.5

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 +11 -18
  8. data/lib/switchman/active_record/associations.rb +315 -0
  9. data/lib/switchman/active_record/attribute_methods.rb +191 -79
  10. data/lib/switchman/active_record/base.rb +204 -50
  11. data/lib/switchman/active_record/calculations.rb +92 -49
  12. data/lib/switchman/active_record/connection_handler.rb +18 -0
  13. data/lib/switchman/active_record/connection_pool.rb +47 -34
  14. data/lib/switchman/active_record/database_configurations.rb +32 -6
  15. data/lib/switchman/active_record/finder_methods.rb +22 -16
  16. data/lib/switchman/active_record/log_subscriber.rb +3 -6
  17. data/lib/switchman/active_record/migration.rb +42 -14
  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 +39 -20
  22. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  23. data/lib/switchman/active_record/query_cache.rb +26 -17
  24. data/lib/switchman/active_record/query_methods.rb +251 -140
  25. data/lib/switchman/active_record/reflection.rb +10 -3
  26. data/lib/switchman/active_record/relation.rb +110 -35
  27. data/lib/switchman/active_record/spawn_methods.rb +3 -7
  28. data/lib/switchman/active_record/statement_cache.rb +13 -9
  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 +89 -0
  32. data/lib/switchman/active_support/cache.rb +25 -4
  33. data/lib/switchman/arel.rb +20 -7
  34. data/lib/switchman/call_super.rb +2 -2
  35. data/lib/switchman/database_server.rb +123 -83
  36. data/lib/switchman/default_shard.rb +14 -5
  37. data/lib/switchman/engine.rb +85 -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 +229 -246
  46. data/lib/switchman/sharded_instrumenter.rb +9 -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 +3 -3
  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 +34 -176
  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: 1224320ec4f3ff0d8f05d1c5537ce06f33932e4c9da069d82183aa7562e1cab4
4
- data.tar.gz: 1cd6f52c82fbdea9c3aac4c569b291428d2476abc4a2198c09beb1da64700473
3
+ metadata.gz: 4676a4e97375d1094b6775fa2a10f907218bbd2cb73f365c801a698822f779a0
4
+ data.tar.gz: 4f88954e9074fcd122ecbb8d965e1b16edb63c63d69fa5311f7e0a97e400c3d5
5
5
  SHA512:
6
- metadata.gz: 052e1675cb5d6b07844dab24c802c6f98b8e75fe4c0146a407e275d89633b5c3058801538a2537f82a7798260ad9d5dd2989536ad88d06a37bb33cf9082444dd
7
- data.tar.gz: ebaa91ea8f6be5f878ac2f8b57a7954853603cb20770f083263fd369774cedc4b33c8127a8b17746bbf4e5b91e1e742df2970be8dab08aa708baae3bc130b988
6
+ metadata.gz: f043f994b2014ce1c980d6fe73a8137a5b497831bbe87592e0c3140e777d10bcbe1460216e3dcf7cc4d1c71cdbda98d3c2b0e5a6a9859f48120cbc2b9425c24f
7
+ data.tar.gz: 943659f21c0c99d8b1db896009a24601a8e698f13d0b2920e3532829a1bf0b1c5a319487d766d24c47b33a975b597ec4dc57e051ee0376e4b4dc6405515e38c8
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:)
11
9
  super
12
10
  end
13
11
  end
@@ -21,34 +19,29 @@ module Switchman
21
19
 
22
20
  def initialize(*args)
23
21
  super
24
- @instrumenter = Switchman::ShardedInstrumenter.new(@instrumenter, self)
22
+
23
+ @instrumenter = Switchman::ShardedInstrumenter.new(@instrumenter, self) if ::Rails.version < "8.0"
24
+
25
25
  @last_query_at = Time.now
26
26
  end
27
27
 
28
- def quote_local_table_name(name)
29
- quote_table_name(name)
28
+ if ::Rails.version >= "8.0"
29
+ def instrumenter # :nodoc:
30
+ @instrumenter ||= Switchman::ShardedInstrumenter.new(::ActiveSupport::Notifications.instrumenter, self)
31
+ end
30
32
  end
31
33
 
32
- def schema_migration
33
- ::ActiveRecord::SchemaMigration
34
+ def quote_local_table_name(name)
35
+ quote_table_name(name)
34
36
  end
35
37
 
36
38
  protected
37
39
 
38
- def log(*args, &block)
40
+ def log(...)
39
41
  super
40
42
  ensure
41
43
  @last_query_at = Time.now
42
44
  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
45
  end
53
46
  end
54
47
  end
@@ -0,0 +1,315 @@
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(async: false)
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
+ elsif ::Rails.version < "8.0"
42
+ super()
43
+ else
44
+ super
45
+ end
46
+ end
47
+ end
48
+
49
+ def _create_record(*)
50
+ shard.activate { super }
51
+ end
52
+ end
53
+
54
+ module BelongsToAssociation
55
+ def replace_keys(record, force: false)
56
+ if record&.class&.sharded_column?(reflection.association_primary_key(record.class))
57
+ foreign_id = record[reflection.association_primary_key(record.class)]
58
+ owner[reflection.foreign_key] = Shard.relative_id_for(foreign_id, record.shard, owner.shard)
59
+ else
60
+ super
61
+ end
62
+ end
63
+
64
+ def shard
65
+ if @owner.class.sharded_column?(@reflection.foreign_key) &&
66
+ (foreign_id = @owner[@reflection.foreign_key])
67
+ Shard.shard_for(foreign_id, @owner.loaded_from_shard)
68
+ else
69
+ super
70
+ end
71
+ end
72
+ end
73
+
74
+ module ForeignAssociation
75
+ # significant change:
76
+ # * transpose the key to the correct shard
77
+ def set_owner_attributes(record) # rubocop:disable Naming/AccessorMethodName
78
+ return if options[:through]
79
+
80
+ key = owner._read_attribute(reflection.join_foreign_key)
81
+ key = Shard.relative_id_for(key, owner.shard, shard)
82
+ record._write_attribute(reflection.join_primary_key, key)
83
+
84
+ record._write_attribute(reflection.type, owner.class.polymorphic_name) if reflection.type
85
+ end
86
+ end
87
+
88
+ module Extension
89
+ def self.build(_model, _reflection); end
90
+
91
+ def self.valid_options
92
+ [:multishard]
93
+ end
94
+ end
95
+
96
+ ::ActiveRecord::Associations::Builder::Association.extensions << Extension
97
+
98
+ module Preloader
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
+
164
+ # Copypasta from Activerecord but with added global_id_for goodness.
165
+ def records_for(ids)
166
+ scope.where(association_key_name => ids).load do |record|
167
+ global_key = if model.connection_class_for_self == UnshardedRecord
168
+ convert_key(record[association_key_name])
169
+ else
170
+ Shard.global_id_for(record[association_key_name], record.shard)
171
+ end
172
+ owner = owners_by_key[convert_key(global_key)].first
173
+ association = owner.association(reflection.name)
174
+ association.set_inverse_instance(record)
175
+ end
176
+ end
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
+
198
+ # significant changes:
199
+ # * partition_by_shard the records_for call
200
+ # * re-globalize the fetched owner id before looking up in the map
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)
204
+ # owners can be duplicated when a relation has a collection association join
205
+ # #compare_by_identity makes such owners different hash keys
206
+ @records_by_owner = {}.compare_by_identity
207
+
208
+ raw_records ||= loader_query.records_for([self])
209
+
210
+ @preloaded_records = raw_records.select do |record|
211
+ assignments = false
212
+
213
+ owner_key = record[association_key_name]
214
+ if owner_key && record.class.sharded_column?(association_key_name)
215
+ owner_key = Shard.global_id_for(owner_key,
216
+ record.shard)
217
+ end
218
+
219
+ owners_by_key[convert_key(owner_key)]&.each do |owner|
220
+ entries = (@records_by_owner[owner] ||= [])
221
+
222
+ if reflection.collection? || entries.empty?
223
+ entries << record
224
+ assignments = true
225
+ end
226
+ end
227
+
228
+ assignments
229
+ end
230
+ end
231
+
232
+ # significant change: globalize keys on sharded columns
233
+ def owners_by_key
234
+ @owners_by_key ||= owners.each_with_object({}) do |owner, result|
235
+ key = owner[owner_key_name]
236
+ key = Shard.global_id_for(key, owner.shard) if key && owner.class.sharded_column?(owner_key_name)
237
+ key = convert_key(key)
238
+ (result[key] ||= []) << owner if key
239
+ end
240
+ end
241
+
242
+ # significant change: don't cache scope (since it could be for different shards)
243
+ def scope
244
+ build_scope
245
+ end
246
+ end
247
+ end
248
+
249
+ module CollectionProxy
250
+ def initialize(*args)
251
+ super
252
+ self.shard_value = scope.shard_value
253
+ self.shard_source_value = :association
254
+ end
255
+
256
+ def shard(*)
257
+ scope.shard(*)
258
+ end
259
+ end
260
+
261
+ module AutosaveAssociation
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)
270
+ end
271
+
272
+ def save_belongs_to_association(reflection)
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
311
+ end
312
+ end
313
+ end
314
+ end
315
+ end