switchman 2.2.3 → 3.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 +10 -2
  3. data/app/models/switchman/shard.rb +272 -275
  4. data/app/models/switchman/unsharded_record.rb +7 -0
  5. data/db/migrate/20130328212039_create_switchman_shards.rb +1 -1
  6. data/db/migrate/20130328224244_create_default_shard.rb +5 -5
  7. data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +1 -0
  8. data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
  9. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +7 -5
  10. data/db/migrate/20190114212900_add_unique_name_indexes.rb +5 -3
  11. data/lib/switchman/action_controller/caching.rb +2 -2
  12. data/lib/switchman/active_record/abstract_adapter.rb +1 -0
  13. data/lib/switchman/active_record/association.rb +78 -89
  14. data/lib/switchman/active_record/attribute_methods.rb +58 -73
  15. data/lib/switchman/active_record/base.rb +59 -60
  16. data/lib/switchman/active_record/calculations.rb +74 -67
  17. data/lib/switchman/active_record/connection_pool.rb +14 -43
  18. data/lib/switchman/active_record/database_configurations/database_config.rb +13 -0
  19. data/lib/switchman/active_record/database_configurations.rb +34 -0
  20. data/lib/switchman/active_record/finder_methods.rb +11 -16
  21. data/lib/switchman/active_record/log_subscriber.rb +4 -5
  22. data/lib/switchman/active_record/migration.rb +7 -51
  23. data/lib/switchman/active_record/model_schema.rb +1 -1
  24. data/lib/switchman/active_record/persistence.rb +3 -14
  25. data/lib/switchman/active_record/postgresql_adapter.rb +125 -169
  26. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  27. data/lib/switchman/active_record/query_cache.rb +18 -19
  28. data/lib/switchman/active_record/query_methods.rb +168 -216
  29. data/lib/switchman/active_record/reflection.rb +7 -22
  30. data/lib/switchman/active_record/relation.rb +30 -78
  31. data/lib/switchman/active_record/spawn_methods.rb +27 -29
  32. data/lib/switchman/active_record/statement_cache.rb +18 -35
  33. data/lib/switchman/active_record/tasks/database_tasks.rb +16 -0
  34. data/lib/switchman/active_support/cache.rb +3 -5
  35. data/lib/switchman/arel.rb +13 -8
  36. data/lib/switchman/database_server.rb +121 -142
  37. data/lib/switchman/default_shard.rb +52 -16
  38. data/lib/switchman/engine.rb +62 -59
  39. data/lib/switchman/environment.rb +4 -8
  40. data/lib/switchman/errors.rb +1 -0
  41. data/lib/switchman/guard_rail/relation.rb +5 -7
  42. data/lib/switchman/guard_rail.rb +6 -19
  43. data/lib/switchman/r_spec_helper.rb +29 -37
  44. data/lib/switchman/rails.rb +14 -12
  45. data/lib/switchman/schema_cache.rb +1 -9
  46. data/lib/switchman/sharded_instrumenter.rb +1 -1
  47. data/lib/switchman/standard_error.rb +15 -3
  48. data/lib/switchman/test_helper.rb +6 -4
  49. data/lib/switchman/version.rb +1 -1
  50. data/lib/switchman.rb +3 -5
  51. data/lib/tasks/switchman.rake +55 -71
  52. metadata +88 -46
  53. data/lib/switchman/active_record/batches.rb +0 -11
  54. data/lib/switchman/active_record/connection_handler.rb +0 -190
  55. data/lib/switchman/active_record/where_clause_factory.rb +0 -36
  56. data/lib/switchman/connection_pool_proxy.rb +0 -173
@@ -6,44 +6,24 @@ require 'switchman/environment'
6
6
  require 'switchman/errors'
7
7
 
8
8
  module Switchman
9
- class Shard < ::ActiveRecord::Base
9
+ class Shard < UnshardedRecord
10
10
  # ten trillion possible ids per shard. yup.
11
11
  IDS_PER_SHARD = 10_000_000_000_000
12
12
 
13
- CATEGORIES =
14
- {
15
- # special cased to mean all other models
16
- :primary => nil,
17
- # special cased to not allow activating a shard other than the default
18
- :unsharded => [Shard]
19
- }
20
- private_constant :CATEGORIES
21
- @connection_specification_name = 'unsharded'
22
-
23
- if defined?(::ProtectedAttributes)
24
- attr_accessible :default, :name, :database_server
25
- end
26
-
27
13
  # only allow one default
28
- validates_uniqueness_of :default, :if => lambda { |s| s.default? }
14
+ validates_uniqueness_of :default, if: ->(s) { s.default? }
29
15
 
30
16
  after_save :clear_cache
31
17
  after_destroy :clear_cache
32
18
 
33
- after_rollback :on_rollback
34
-
35
19
  scope :primary, -> { where(name: nil).order(:database_server_id, :id).distinct_on(:database_server_id) }
36
20
 
37
21
  class << self
38
- def categories
39
- CATEGORIES.keys
22
+ def sharded_models
23
+ @sharded_models ||= [::ActiveRecord::Base, UnshardedRecord].freeze
40
24
  end
41
25
 
42
- def default(reload_deprecated = false, reload: false, with_fallback: false)
43
- if reload_deprecated
44
- reload = reload_deprecated
45
- ::ActiveSupport::Deprecation.warn("positional reload parameter to Switchman::Shard.default is deprecated; use `reload: true`")
46
- end
26
+ def default(reload: false, with_fallback: false)
47
27
  if !@default || reload
48
28
  # Have to create a dummy object so that several key methods still work
49
29
  # (it's easier to do this in one place here, and just assume that sharding
@@ -54,74 +34,68 @@ module Switchman
54
34
 
55
35
  # if we already have a default shard in place, and the caller wants
56
36
  # to use it as a fallback, use that instead of the dummy instance
57
- if with_fallback && @default
58
- default = @default
59
- end
37
+ default = @default if with_fallback && @default
60
38
 
61
39
  # the first time we need a dummy dummy for re-entrancy to avoid looping on ourselves
62
40
  @default ||= default
63
41
 
64
42
  # Now find the actual record, if it exists; rescue the fake default if the table doesn't exist
65
43
  @default = begin
66
- find_cached("default_shard") { Shard.where(default: true).take } || default
44
+ find_cached('default_shard') { Shard.where(default: true).take } || default
67
45
  rescue
68
46
  default
69
47
  end
70
48
 
71
- # rebuild current shard activations - it might have "another" default shard serialized there
72
- active_shards.replace(active_shards.dup.map do |category, shard|
73
- shard = Shard.lookup((!shard || shard.default?) ? 'default' : shard.id)
74
- [category, shard]
75
- end.to_h)
76
-
77
- activate!(primary: @default) if active_shards.empty?
78
-
79
49
  # make sure this is not erroneously cached
80
- if @default.database_server.instance_variable_defined?(:@primary_shard)
81
- @default.database_server.remove_instance_variable(:@primary_shard)
82
- end
50
+ @default.database_server.remove_instance_variable(:@primary_shard) if @default.database_server.instance_variable_defined?(:@primary_shard)
83
51
 
84
52
  # and finally, check for cached references to the default shard on the existing connection
85
- if ::ActiveRecord::Base.connected? && ::ActiveRecord::Base.connection.shard.default?
86
- ::ActiveRecord::Base.connection.shard = @default
53
+ sharded_models.each do |klass|
54
+ klass.connection.shard = @default if klass.connected? && klass.connection.shard.default?
87
55
  end
88
56
  end
89
57
  @default
90
58
  end
91
59
 
92
- def current(category = :primary)
93
- active_shards[category] || Shard.default
60
+ def current(klass = ::ActiveRecord::Base)
61
+ klass ||= ::ActiveRecord::Base
62
+ klass.connection_pool.shard
94
63
  end
95
64
 
96
65
  def activate(shards)
97
- old_shards = activate!(shards)
66
+ activated_classes = activate!(shards)
98
67
  yield
99
68
  ensure
100
- active_shards.merge!(old_shards) if old_shards
69
+ activated_classes.each do |klass|
70
+ klass.connection_pool.shard_stack.pop
71
+ klass.connected_to_stack.pop
72
+ end
101
73
  end
102
74
 
103
75
  def activate!(shards)
104
- old_shards = nil
105
- currently_active_shards = active_shards
106
- shards.each do |category, shard|
107
- next if category == :unsharded
108
- unless currently_active_shards[category] == shard
109
- old_shards ||= {}
110
- old_shards[category] = currently_active_shards[category]
111
- currently_active_shards[category] = shard
112
- end
76
+ activated_classes = []
77
+ shards.each do |klass, shard|
78
+ next if klass == UnshardedRecord
79
+
80
+ next unless klass.current_shard != shard.database_server.id.to_sym ||
81
+ klass.connection_pool.shard != shard
82
+
83
+ activated_classes << klass
84
+ klass.connected_to_stack << { shard: shard.database_server.id.to_sym, klasses: [klass] }
85
+ klass.connection_pool.shard_stack << shard
113
86
  end
114
- old_shards
87
+ activated_classes
115
88
  end
116
89
 
117
90
  def lookup(id)
118
91
  id_i = id.to_i
119
92
  return current if id_i == current.id || id == 'self'
120
93
  return default if id_i == default.id || id.nil? || id == 'default'
94
+
121
95
  id = id_i
122
- raise ArgumentError if id == 0
96
+ raise ArgumentError if id.zero?
123
97
 
124
- unless cached_shards.has_key?(id)
98
+ unless cached_shards.key?(id)
125
99
  cached_shards[id] = Shard.default.activate do
126
100
  find_cached(['shard', id]) { find_by(id: id) }
127
101
  end
@@ -129,11 +103,6 @@ module Switchman
129
103
  cached_shards[id]
130
104
  end
131
105
 
132
- def preload_cache
133
- cached_shards.reverse_merge!(active_shards.values.index_by(&:id))
134
- cached_shards.reverse_merge!(all.index_by(&:id))
135
- end
136
-
137
106
  def clear_cache
138
107
  cached_shards.clear
139
108
  end
@@ -141,63 +110,77 @@ module Switchman
141
110
  # ==== Parameters
142
111
  #
143
112
  # * +shards+ - an array or relation of Shards to iterate over
144
- # * +categories+ - an array of categories to activate
145
- # * +options+ -
146
- # :parallel - true/false to execute in parallel, or a integer of how many
147
- # sub-processes. Note that parallel invocation currently uses
148
- # forking, so should be used sparingly because you cannot get
149
- # results back
150
- # :exception - :ignore, :raise, :defer (wait until the end and raise the first
113
+ # * +classes+ - an array of classes to activate
114
+ # parallel: - true/false to execute in parallel, or a integer of how many
115
+ # sub-processes per database server. Note that parallel
116
+ # invocation currently uses forking, so should be used sparingly
117
+ # because errors are not raised, and you cannot get results back
118
+ # max_procs: - only run this many parallel processes at a time
119
+ # exception: - :ignore, :raise, :defer (wait until the end and raise the first
151
120
  # error), or a proc
152
- def with_each_shard(*args)
153
- raise ArgumentError, "wrong number of arguments (#{args.length} for 0...3)" if args.length > 3
121
+ def with_each_shard(*args, parallel: false, max_procs: nil, exception: :raise, &block)
122
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
154
123
 
155
- unless default.is_a?(Shard)
156
- return Array.wrap(yield)
157
- end
124
+ return Array.wrap(yield) unless default.is_a?(Shard)
158
125
 
159
- options = args.extract_options!
160
126
  if args.length == 1
161
- if Array === args.first && args.first.first.is_a?(Symbol)
162
- categories = args.first
127
+ if Array === args.first && args.first.first.is_a?(Class)
128
+ classes = args.first
163
129
  else
164
130
  scope = args.first
165
131
  end
166
132
  else
167
- scope, categories = args
133
+ scope, classes = args
168
134
  end
169
135
 
170
- # back-compat
171
- options[:parallel] = options.delete(:max_procs) if options.key?(:max_procs)
172
-
173
- parallel = case options[:parallel]
174
- when true
175
- [Environment.cpu_count || 2, 2].min
176
- when false, nil
177
- 1
178
- else
179
- options[:parallel]
180
- end
181
- options.delete(:parallel)
136
+ parallel = 1 if parallel == true
137
+ parallel = 0 if parallel == false || parallel.nil?
182
138
 
183
139
  scope ||= Shard.all
184
- if ::ActiveRecord::Relation === scope && scope.order_values.empty?
185
- scope = scope.order(::Arel.sql("database_server_id IS NOT NULL, database_server_id, id"))
186
- end
140
+ scope = scope.order(::Arel.sql('database_server_id IS NOT NULL, database_server_id, id')) if ::ActiveRecord::Relation === scope && scope.order_values.empty?
187
141
 
188
- if parallel > 1
142
+ if parallel.positive?
143
+ max_procs = determine_max_procs(max_procs, parallel)
189
144
  if ::ActiveRecord::Relation === scope
190
145
  # still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
191
146
  database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
192
- map(&:database_server).compact.uniq
147
+ map(&:database_server).compact.uniq
193
148
  # nothing to do
194
- return if database_servers.count == 0
149
+ return if database_servers.count.zero?
195
150
 
196
- scopes = Hash[database_servers.map do |server|
197
- [server, server.shards.merge(scope)]
198
- end]
151
+ parallel = [(max_procs.to_f / database_servers.count).ceil, parallel].min if max_procs
152
+
153
+ scopes = database_servers.map do |server|
154
+ server_scope = server.shards.merge(scope)
155
+ if parallel == 1
156
+ subscopes = [server_scope]
157
+ else
158
+ subscopes = []
159
+ total = server_scope.count
160
+ ranges = []
161
+ server_scope.find_ids_in_ranges(batch_size: (total.to_f / parallel).ceil) do |min, max|
162
+ ranges << [min, max]
163
+ end
164
+ # create a half-open range on the last one
165
+ ranges.last[1] = nil
166
+ ranges.each do |min, max|
167
+ subscope = server_scope.where('id>=?', min)
168
+ subscope = subscope.where('id<=?', max) if max
169
+ subscopes << subscope
170
+ end
171
+ end
172
+ [server, subscopes]
173
+ end.to_h
199
174
  else
200
175
  scopes = scope.group_by(&:database_server)
176
+ if parallel > 1
177
+ parallel = [(max_procs.to_f / scopes.count).ceil, parallel].min if max_procs
178
+ scopes = scopes.map do |(server, shards)|
179
+ [server, shards.in_groups(parallel, false).compact]
180
+ end.to_h
181
+ else
182
+ scopes = scopes.map { |(server, shards)| [server, [shards]] }.to_h
183
+ end
201
184
  end
202
185
 
203
186
  exception_pipes = []
@@ -208,8 +191,8 @@ module Switchman
208
191
  fd_to_name_map = {}
209
192
  errors = []
210
193
 
211
- wait_for_output = lambda do |out_fds, err_fds, fd_to_name_map|
212
- ready, _ = IO.select(out_fds + err_fds)
194
+ wait_for_output = lambda do
195
+ ready, = IO.select(out_fds + err_fds)
213
196
  ready.each do |fd|
214
197
  if fd.eof?
215
198
  fd.close
@@ -223,8 +206,9 @@ module Switchman
223
206
  end
224
207
 
225
208
  # only one process; don't bother forking
226
- if scopes.length == 1
227
- return with_each_shard(scopes.first.last, categories, options) { yield }
209
+ if scopes.length == 1 && parallel == 1
210
+ return with_each_shard(scopes.first.last.first, classes, exception: exception,
211
+ &block)
228
212
  end
229
213
 
230
214
  # clear connections prior to forking (no more queries will be executed in the parent,
@@ -232,13 +216,17 @@ module Switchman
232
216
  # silly like dealloc'ing prepared statements)
233
217
  ::ActiveRecord::Base.clear_all_connections!
234
218
 
235
- scopes.each do |server, subscope|
236
- name = server.id
219
+ scopes.each do |server, subscopes|
220
+ subscopes.each_with_index do |subscope, idx|
221
+ name = if subscopes.length > 1
222
+ "#{server.id} #{idx + 1}"
223
+ else
224
+ server.id
225
+ end
237
226
 
238
- exception_pipe = IO.pipe
239
- exception_pipes << exception_pipe
240
- pid, io_in, io_out, io_err = Open4.pfork4(lambda do
241
- begin
227
+ exception_pipe = IO.pipe
228
+ exception_pipes << exception_pipe
229
+ pid, io_in, io_out, io_err = Open4.pfork4(lambda do
242
230
  Switchman.config[:on_fork_proc]&.call
243
231
 
244
232
  # set a pretty name for the process title, up to 128 characters
@@ -246,70 +234,55 @@ module Switchman
246
234
  # was started)
247
235
  # first, simplify the binary name by stripping directories,
248
236
  # then truncate arguments as necessary
249
- bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
237
+ bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
250
238
  max_length = 128 - bin.length - name.length - 3
251
- args = ARGV.join(" ")
252
- if max_length >= 0
253
- args = args[0..max_length]
254
- end
255
- new_title = [bin, args, name].join(" ")
239
+ args = ARGV.join(' ')
240
+ args = args[0..max_length] if max_length >= 0
241
+ new_title = [bin, args, name].join(' ')
256
242
  Process.setproctitle(new_title)
257
243
 
258
- with_each_shard(subscope, categories, options) { yield }
244
+ with_each_shard(subscope, classes, exception: exception, &block)
259
245
  exception_pipe.last.close
260
- rescue Exception => e
246
+ rescue => e
261
247
  begin
262
248
  dumped = Marshal.dump(e)
263
- dumped = nil if dumped.length > 64 * 1024
264
249
  rescue
265
- dumped = nil
266
- end
267
-
268
- if dumped.nil?
269
250
  # couldn't dump the exception; create a copy with just
270
251
  # the message and the backtrace
271
252
  e2 = e.class.new(e.message)
272
- backtrace = e.backtrace
273
- # truncate excessively long backtraces
274
- if backtrace.length > 50
275
- backtrace = backtrace[0...25] + ['...'] + backtrace[-25..-1]
276
- end
277
- e2.set_backtrace(backtrace)
253
+ e2.set_backtrace(e.backtrace)
278
254
  e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
279
255
  dumped = Marshal.dump(e2)
280
256
  end
281
-
282
257
  exception_pipe.last.set_encoding(dumped.encoding)
283
258
  exception_pipe.last.write(dumped)
284
259
  exception_pipe.last.flush
285
260
  exception_pipe.last.close
286
261
  exit! 1
262
+ end)
263
+ exception_pipe.last.close
264
+ pids << pid
265
+ io_in.close # don't care about writing to stdin
266
+ out_fds << io_out
267
+ err_fds << io_err
268
+ pid_to_name_map[pid] = name
269
+ fd_to_name_map[io_out] = name
270
+ fd_to_name_map[io_err] = name
271
+
272
+ while max_procs && pids.count >= max_procs
273
+ while max_procs && out_fds.count >= max_procs
274
+ # wait for output if we've hit the max_procs limit
275
+ wait_for_output.call
276
+ end
277
+ # we've gotten all the output from one fd so wait for its child process to exit
278
+ found_pid, status = Process.wait2
279
+ pids.delete(found_pid)
280
+ errors << pid_to_name_map[found_pid] if status.exitstatus != 0
287
281
  end
288
- end)
289
- exception_pipe.last.close
290
- pids << pid
291
- io_in.close # don't care about writing to stdin
292
- out_fds << io_out
293
- err_fds << io_err
294
- pid_to_name_map[pid] = name
295
- fd_to_name_map[io_out] = name
296
- fd_to_name_map[io_err] = name
297
-
298
- while pids.count >= parallel
299
- while out_fds.count >= parallel
300
- # wait for output if we've hit the parallel limit
301
- wait_for_output.call(out_fds, err_fds, fd_to_name_map)
302
- end
303
- # we've gotten all the output from one fd so wait for its child process to exit
304
- found_pid, status = Process.wait2
305
- pids.delete(found_pid)
306
- errors << pid_to_name_map[found_pid] if status.exitstatus != 0
307
282
  end
308
283
  end
309
284
 
310
- while out_fds.any? || err_fds.any?
311
- wait_for_output.call(out_fds, err_fds, fd_to_name_map)
312
- end
285
+ wait_for_output.call while out_fds.any? || err_fds.any?
313
286
  pids.each do |pid|
314
287
  _, status = Process.waitpid2(pid)
315
288
  errors << pid_to_name_map[pid] if status.exitstatus != 0
@@ -317,67 +290,51 @@ module Switchman
317
290
 
318
291
  # check for an exception; we only re-raise the first one
319
292
  exception_pipes.each do |exception_pipe|
320
- begin
321
- serialized_exception = exception_pipe.first.read
322
- next if serialized_exception.empty?
323
- exception = Marshal.load(serialized_exception)
324
- raise exception
325
- ensure
326
- exception_pipe.first.close
327
- end
293
+ serialized_exception = exception_pipe.first.read
294
+ next if serialized_exception.empty?
295
+
296
+ ex = Marshal.load(serialized_exception) # rubocop:disable Security/MarshalLoad
297
+ raise ex
298
+ ensure
299
+ exception_pipe.first.close
328
300
  end
329
301
 
330
302
  unless errors.empty?
331
- raise ParallelShardExecError.new("The following subprocesses did not exit cleanly: #{errors.sort.join(", ")}")
303
+ raise ParallelShardExecError,
304
+ "The following subprocesses did not exit cleanly: #{errors.sort.join(', ')}"
332
305
  end
306
+
333
307
  return
334
308
  end
335
309
 
336
- categories ||= []
310
+ classes ||= []
337
311
 
338
312
  previous_shard = nil
339
- close_connections_if_needed = lambda do |shard|
340
- # prune the prior connection unless it happened to be the same
341
- if previous_shard && shard != previous_shard && !previous_shard.database_server.shareable?
342
- previous_shard.activate do
343
- ::GuardRail.activated_environments.each do |env|
344
- ::GuardRail.activate(env) do
345
- if ::ActiveRecord::Base.connected? && ::ActiveRecord::Base.connection.open_transactions == 0
346
- ::ActiveRecord::Base.connection_pool.current_pool.disconnect!
347
- end
348
- end
349
- end
350
- end
351
- end
352
- end
353
-
354
313
  result = []
355
- exception = nil
314
+ ex = nil
356
315
  scope.each do |shard|
357
316
  # shard references a database server that isn't configured in this environment
358
317
  next unless shard.database_server
359
- close_connections_if_needed.call(shard)
360
- shard.activate(*categories) do
361
- begin
362
- result.concat Array.wrap(yield)
363
- rescue
364
- case options[:exception]
365
- when :ignore
366
- when :defer
367
- exception ||= $!
368
- when Proc
369
- options[:exception].call
370
- when :raise
371
- raise
372
- else
373
- raise
374
- end
318
+
319
+ shard.activate(*classes) do
320
+ result.concat Array.wrap(yield)
321
+ rescue
322
+ case exception
323
+ when :ignore
324
+ # ignore
325
+ when :defer
326
+ ex ||= $!
327
+ when Proc
328
+ exception.call
329
+ # when :raise
330
+ else
331
+ raise
375
332
  end
376
333
  end
377
334
  previous_shard = shard
378
335
  end
379
- close_connections_if_needed.call(Shard.current)
380
- raise exception if exception
336
+ raise ex if ex
337
+
381
338
  result
382
339
  end
383
340
 
@@ -386,22 +343,22 @@ module Switchman
386
343
  array.each do |object|
387
344
  partition_object = partition_proc ? partition_proc.call(object) : object
388
345
  case partition_object
389
- when Shard
390
- shard = partition_object
391
- when ::ActiveRecord::Base
392
- if partition_object.respond_to?(:associated_shards)
393
- partition_object.associated_shards.each do |a_shard|
394
- shard_arrays[a_shard] ||= []
395
- shard_arrays[a_shard] << object
396
- end
397
- next
398
- else
399
- shard = partition_object.shard
346
+ when Shard
347
+ shard = partition_object
348
+ when ::ActiveRecord::Base
349
+ if partition_object.respond_to?(:associated_shards)
350
+ partition_object.associated_shards.each do |a_shard|
351
+ shard_arrays[a_shard] ||= []
352
+ shard_arrays[a_shard] << object
400
353
  end
401
- when Integer, /^\d+$/, /^(\d+)~(\d+)$/
402
- local_id, shard = Shard.local_id_for(partition_object)
403
- local_id ||= partition_object
404
- object = local_id if !partition_proc
354
+ next
355
+ else
356
+ shard = partition_object.shard
357
+ end
358
+ when Integer, /^\d+$/, /^(\d+)~(\d+)$/
359
+ local_id, shard = Shard.local_id_for(partition_object)
360
+ local_id ||= partition_object
361
+ object = local_id unless partition_proc
405
362
  end
406
363
  shard ||= Shard.current
407
364
  shard_arrays[shard] ||= []
@@ -410,7 +367,7 @@ module Switchman
410
367
  # TODO: use with_each_shard (or vice versa) to get
411
368
  # connection management and parallelism benefits
412
369
  shard_arrays.inject([]) do |results, (shard, objects)|
413
- results.concat shard.activate { Array.wrap(yield objects) }
370
+ results.concat(shard.activate { Array.wrap(yield objects) })
414
371
  end
415
372
  end
416
373
 
@@ -422,7 +379,7 @@ module Switchman
422
379
  # stay as provided. This assumes no consumer
423
380
  # will return a nil value from the block.
424
381
  def signed_id_operation(input_id)
425
- sign = input_id < 0 ? -1 : 1
382
+ sign = input_id.negative? ? -1 : 1
426
383
  output = yield input_id.abs
427
384
  output * sign
428
385
  end
@@ -430,9 +387,10 @@ module Switchman
430
387
  # converts an AR object, integral id, string id, or string short-global-id to a
431
388
  # integral id. nil if it can't be interpreted
432
389
  def integral_id_for(any_id)
433
- if any_id.is_a?(::Arel::Nodes::Casted)
434
- any_id = any_id.val
435
- elsif any_id.is_a?(::Arel::Nodes::BindParam) && ::Rails.version >= "5.2"
390
+ case any_id
391
+ when ::Arel::Nodes::Casted
392
+ any_id = any_id.value
393
+ when ::Arel::Nodes::BindParam
436
394
  any_id = any_id.value.value_before_type_cast
437
395
  end
438
396
 
@@ -443,12 +401,11 @@ module Switchman
443
401
  local_id = $2.to_i
444
402
  signed_id_operation(local_id) do |id|
445
403
  return nil if id > IDS_PER_SHARD
404
+
446
405
  $1.to_i * IDS_PER_SHARD + id
447
406
  end
448
407
  when Integer, /^-?\d+$/
449
408
  any_id.to_i
450
- else
451
- nil
452
409
  end
453
410
  end
454
411
 
@@ -460,11 +417,12 @@ module Switchman
460
417
  def local_id_for(any_id)
461
418
  id = integral_id_for(any_id)
462
419
  return NIL_NIL_ID unless id
420
+
463
421
  return_shard = nil
464
422
  local_id = signed_id_operation(id) do |abs_id|
465
423
  if abs_id < IDS_PER_SHARD
466
424
  abs_id
467
- elsif return_shard = lookup(abs_id / IDS_PER_SHARD)
425
+ elsif (return_shard = lookup(abs_id / IDS_PER_SHARD))
468
426
  abs_id % IDS_PER_SHARD
469
427
  else
470
428
  return NIL_NIL_ID
@@ -481,8 +439,10 @@ module Switchman
481
439
  integral_id = integral_id_for(any_id)
482
440
  local_id, shard = local_id_for(integral_id)
483
441
  return integral_id unless local_id
442
+
484
443
  shard ||= source_shard
485
444
  return local_id if shard == target_shard
445
+
486
446
  shard.global_id_for(local_id)
487
447
  end
488
448
 
@@ -493,6 +453,7 @@ module Switchman
493
453
  local_id, shard = local_id_for(any_id)
494
454
  return any_id unless local_id
495
455
  return local_id unless shard
456
+
496
457
  "#{shard.id}~#{local_id}"
497
458
  end
498
459
 
@@ -501,6 +462,7 @@ module Switchman
501
462
  def global_id_for(any_id, source_shard = nil)
502
463
  id = integral_id_for(any_id)
503
464
  return any_id unless id
465
+
504
466
  signed_id_operation(id) do |abs_id|
505
467
  if abs_id >= IDS_PER_SHARD
506
468
  abs_id
@@ -513,11 +475,59 @@ module Switchman
513
475
 
514
476
  def shard_for(any_id, source_shard = nil)
515
477
  return any_id.shard if any_id.is_a?(::ActiveRecord::Base)
478
+
516
479
  _, shard = local_id_for(any_id)
517
480
  shard || source_shard || Shard.current
518
481
  end
519
482
 
483
+ # given the provided option, determines whether we need to (and whether
484
+ # it's possible) to determine a reasonable default.
485
+ def determine_max_procs(max_procs_input, parallel_input = 2)
486
+ max_procs = nil
487
+ if max_procs_input
488
+ max_procs = max_procs_input.to_i
489
+ max_procs = nil if max_procs.zero?
490
+ else
491
+ return 1 if parallel_input.nil? || parallel_input < 1
492
+
493
+ cpus = Environment.cpu_count
494
+ max_procs = cpus * parallel_input if cpus&.positive?
495
+ end
496
+
497
+ max_procs
498
+ end
499
+
520
500
  private
501
+
502
+ def add_sharded_model(klass)
503
+ @sharded_models = (sharded_models + [klass]).freeze
504
+ initialize_sharding
505
+ end
506
+
507
+ def initialize_sharding
508
+ full_connects_to_hash = DatabaseServer.all.map { |db| [db.id.to_sym, db.connects_to_hash] }.to_h
509
+ sharded_models.each do |klass|
510
+ connects_to_hash = full_connects_to_hash.deep_dup
511
+ if klass == UnshardedRecord
512
+ # no need to mention other databases for the unsharded category
513
+ connects_to_hash = { ::Rails.env.to_sym => DatabaseServer.find(nil).connects_to_hash }
514
+ end
515
+
516
+ # prune things we're already connected to
517
+ if klass.connection_specification_name == klass.name
518
+ connects_to_hash.each do |(db_name, role_hash)|
519
+ role_hash.each_key do |role|
520
+ role_hash.delete(role) if klass.connection_handler.retrieve_connection_pool(
521
+ klass.connection_specification_name, role: role, shard: db_name
522
+ )
523
+ end
524
+ end
525
+ end
526
+
527
+ klass.connects_to shards: connects_to_hash
528
+ end
529
+ end
530
+
521
531
  # in-process caching
522
532
  def cached_shards
523
533
  @cached_shards ||= {}.compare_by_identity
@@ -548,10 +558,6 @@ module Switchman
548
558
  shard = nil unless shard.database_server
549
559
  shard
550
560
  end
551
-
552
- def active_shards
553
- Thread.current[:active_shards] ||= {}.compare_by_identity
554
- end
555
561
  end
556
562
 
557
563
  def name
@@ -565,11 +571,11 @@ module Switchman
565
571
 
566
572
  def name=(name)
567
573
  write_attribute(:name, @name = name)
568
- remove_instance_variable(:@name) if name == nil
574
+ remove_instance_variable(:@name) if name.nil?
569
575
  end
570
576
 
571
577
  def database_server
572
- @database_server ||= DatabaseServer.find(self.database_server_id)
578
+ @database_server ||= DatabaseServer.find(database_server_id)
573
579
  end
574
580
 
575
581
  def database_server=(database_server)
@@ -590,23 +596,21 @@ module Switchman
590
596
  Shard.default
591
597
  end
592
598
 
593
- def activate(*categories)
594
- shards = hashify_categories(categories)
595
- Shard.activate(shards) do
596
- yield
597
- end
599
+ def activate(*classes, &block)
600
+ shards = hashify_classes(classes)
601
+ Shard.activate(shards, &block)
598
602
  end
599
603
 
600
604
  # for use from console ONLY
601
- def activate!(*categories)
602
- shards = hashify_categories(categories)
605
+ def activate!(*classes)
606
+ shards = hashify_classes(classes)
603
607
  Shard.activate!(shards)
604
608
  nil
605
609
  end
606
610
 
607
611
  # custom serialization, since shard is self-referential
608
- def _dump(depth)
609
- self.id.to_s
612
+ def _dump(_depth)
613
+ id.to_s
610
614
  end
611
615
 
612
616
  def self._load(str)
@@ -614,45 +618,45 @@ module Switchman
614
618
  end
615
619
 
616
620
  def drop_database
617
- raise("Cannot drop the database of the default shard") if self.default?
621
+ raise('Cannot drop the database of the default shard') if default?
618
622
  return unless read_attribute(:name)
619
623
 
620
624
  begin
621
- adapter = self.database_server.config[:adapter]
625
+ adapter = database_server.config[:adapter]
622
626
  sharding_config = Switchman.config || {}
623
627
  drop_statement = sharding_config[adapter]&.[](:drop_statement)
624
628
  drop_statement ||= sharding_config[:drop_statement]
625
629
  if drop_statement
626
630
  drop_statement = Array(drop_statement).dup.
627
- map { |statement| statement.gsub('%{name}', self.name) }
631
+ map { |statement| statement.gsub('%{name}', name) }
628
632
  end
629
633
 
630
634
  case adapter
631
- when 'mysql', 'mysql2'
632
- self.activate do
633
- ::GuardRail.activate(:deploy) do
634
- drop_statement ||= "DROP DATABASE #{self.name}"
635
- Array(drop_statement).each do |stmt|
636
- ::ActiveRecord::Base.connection.execute(stmt)
637
- end
635
+ when 'mysql', 'mysql2'
636
+ activate do
637
+ ::GuardRail.activate(:deploy) do
638
+ drop_statement ||= "DROP DATABASE #{name}"
639
+ Array(drop_statement).each do |stmt|
640
+ ::ActiveRecord::Base.connection.execute(stmt)
638
641
  end
639
642
  end
640
- when 'postgresql'
641
- self.activate do
642
- ::GuardRail.activate(:deploy) do
643
- # Shut up, Postgres!
644
- conn = ::ActiveRecord::Base.connection
645
- old_proc = conn.raw_connection.set_notice_processor {}
646
- begin
647
- drop_statement ||= "DROP SCHEMA #{self.name} CASCADE"
648
- Array(drop_statement).each do |stmt|
649
- ::ActiveRecord::Base.connection.execute(stmt)
650
- end
651
- ensure
652
- conn.raw_connection.set_notice_processor(&old_proc) if old_proc
643
+ end
644
+ when 'postgresql'
645
+ activate do
646
+ ::GuardRail.activate(:deploy) do
647
+ # Shut up, Postgres!
648
+ conn = ::ActiveRecord::Base.connection
649
+ old_proc = conn.raw_connection.set_notice_processor {}
650
+ begin
651
+ drop_statement ||= "DROP SCHEMA #{name} CASCADE"
652
+ Array(drop_statement).each do |stmt|
653
+ ::ActiveRecord::Base.connection.execute(stmt)
653
654
  end
655
+ ensure
656
+ conn.raw_connection.set_notice_processor(&old_proc) if old_proc
654
657
  end
655
658
  end
659
+ end
656
660
  end
657
661
  rescue
658
662
  logger.info "Drop failed: #{$!}"
@@ -662,8 +666,9 @@ module Switchman
662
666
  # takes an id local to this shard, and returns a global id
663
667
  def global_id_for(local_id)
664
668
  return nil unless local_id
669
+
665
670
  self.class.signed_id_operation(local_id) do |abs_id|
666
- abs_id + self.id * IDS_PER_SHARD
671
+ abs_id + id * IDS_PER_SHARD
667
672
  end
668
673
  end
669
674
 
@@ -673,7 +678,8 @@ module Switchman
673
678
  end
674
679
 
675
680
  def destroy
676
- raise("Cannot destroy the default shard") if self.default?
681
+ raise('Cannot destroy the default shard') if default?
682
+
677
683
  super
678
684
  end
679
685
 
@@ -682,31 +688,22 @@ module Switchman
682
688
  def clear_cache
683
689
  Shard.default.activate do
684
690
  Switchman.cache.delete(['shard', id].join('/'))
685
- Switchman.cache.delete("default_shard") if default?
691
+ Switchman.cache.delete('default_shard') if default?
686
692
  end
687
- self.class.clear_cache
688
693
  end
689
694
 
690
695
  def default_name
691
696
  database_server.shard_name(self)
692
697
  end
693
698
 
694
- def on_rollback
695
- # make sure all connection pool proxies are referencing valid pools
696
- ::ActiveRecord::Base.connection_handler.connection_pools.each do |pool|
697
- next unless pool.is_a?(ConnectionPoolProxy)
698
-
699
- pool.remove_shard!(self)
700
- end
701
- end
702
-
703
- def hashify_categories(categories)
704
- if categories.empty?
705
- { :primary => self }
699
+ def hashify_classes(classes)
700
+ if classes.empty?
701
+ { ::ActiveRecord::Base => self }
706
702
  else
707
- categories.inject({}) { |h, category| h[category] = self; h }
703
+ classes.each_with_object({}) do |klass, h|
704
+ h[klass] = self
705
+ end
708
706
  end
709
707
  end
710
-
711
708
  end
712
709
  end