switchman 1.6.1 → 2.0.9

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