switchman 2.2.3 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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