switchman 1.5.21 → 2.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +5 -5
  2. data/app/models/switchman/shard.rb +757 -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 +54 -22
  13. data/lib/switchman/active_record/base.rb +76 -31
  14. data/lib/switchman/active_record/batches.rb +3 -1
  15. data/lib/switchman/active_record/calculations.rb +17 -22
  16. data/lib/switchman/active_record/connection_handler.rb +88 -78
  17. data/lib/switchman/active_record/connection_pool.rb +28 -23
  18. data/lib/switchman/active_record/finder_methods.rb +37 -28
  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 +170 -126
  24. data/lib/switchman/active_record/predicate_builder.rb +3 -1
  25. data/lib/switchman/active_record/query_cache.rb +22 -87
  26. data/lib/switchman/active_record/query_methods.rb +139 -125
  27. data/lib/switchman/active_record/reflection.rb +42 -14
  28. data/lib/switchman/active_record/relation.rb +108 -33
  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 +44 -41
  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 +7 -10
  52. data/lib/switchman/version.rb +3 -1
  53. data/lib/switchman.rb +5 -1
  54. data/lib/tasks/switchman.rake +53 -72
  55. metadata +84 -38
  56. data/app/models/switchman/shard_internal.rb +0 -692
@@ -1,11 +1,757 @@
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 = '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 Exception => e
287
+ begin
288
+ dumped = Marshal.dump(e)
289
+ dumped = nil if dumped.length > 64 * 1024
290
+ rescue
291
+ dumped = nil
292
+ end
293
+
294
+ if dumped.nil?
295
+ # couldn't dump the exception; create a copy with just
296
+ # the message and the backtrace
297
+ e2 = e.class.new(e.message)
298
+ backtrace = e.backtrace
299
+ # truncate excessively long backtraces
300
+ if backtrace.length > 50
301
+ backtrace = backtrace[0...25] + ['...'] + backtrace[-25..-1]
302
+ end
303
+ e2.set_backtrace(backtrace)
304
+ e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
305
+ dumped = Marshal.dump(e2)
306
+ end
307
+
308
+ exception_pipe.last.set_encoding(dumped.encoding)
309
+ exception_pipe.last.write(dumped)
310
+ exception_pipe.last.flush
311
+ exception_pipe.last.close
312
+ exit! 1
313
+ end
314
+ end)
315
+ exception_pipe.last.close
316
+ pids << pid
317
+ io_in.close # don't care about writing to stdin
318
+ out_fds << io_out
319
+ err_fds << io_err
320
+ pid_to_name_map[pid] = name
321
+ fd_to_name_map[io_out] = name
322
+ fd_to_name_map[io_err] = name
323
+
324
+ while max_procs && pids.count >= max_procs
325
+ while max_procs && out_fds.count >= max_procs
326
+ # wait for output if we've hit the max_procs limit
327
+ wait_for_output.call(out_fds, err_fds, fd_to_name_map)
328
+ end
329
+ # we've gotten all the output from one fd so wait for its child process to exit
330
+ found_pid, status = Process.wait2
331
+ pids.delete(found_pid)
332
+ errors << pid_to_name_map[found_pid] if status.exitstatus != 0
333
+ end
334
+ end
335
+ end
336
+
337
+ while out_fds.any? || err_fds.any?
338
+ wait_for_output.call(out_fds, err_fds, fd_to_name_map)
339
+ end
340
+ pids.each do |pid|
341
+ _, status = Process.waitpid2(pid)
342
+ errors << pid_to_name_map[pid] if status.exitstatus != 0
343
+ end
344
+
345
+ # check for an exception; we only re-raise the first one
346
+ exception_pipes.each do |exception_pipe|
347
+ begin
348
+ serialized_exception = exception_pipe.first.read
349
+ next if serialized_exception.empty?
350
+ exception = Marshal.load(serialized_exception)
351
+ raise exception
352
+ ensure
353
+ exception_pipe.first.close
354
+ end
355
+ end
356
+
357
+ unless errors.empty?
358
+ raise ParallelShardExecError.new("The following subprocesses did not exit cleanly: #{errors.sort.join(", ")}")
359
+ end
360
+ return
361
+ end
362
+
363
+ categories ||= []
364
+
365
+ previous_shard = nil
366
+ close_connections_if_needed = lambda do |shard|
367
+ # prune the prior connection unless it happened to be the same
368
+ if previous_shard && shard != previous_shard && !previous_shard.database_server.shareable?
369
+ previous_shard.activate do
370
+ ::GuardRail.activated_environments.each do |env|
371
+ ::GuardRail.activate(env) do
372
+ if ::ActiveRecord::Base.connected? && ::ActiveRecord::Base.connection.open_transactions == 0
373
+ ::ActiveRecord::Base.connection_pool.current_pool.disconnect!
374
+ end
375
+ end
376
+ end
377
+ end
378
+ end
379
+ end
380
+
381
+ result = []
382
+ exception = nil
383
+ scope.each do |shard|
384
+ # shard references a database server that isn't configured in this environment
385
+ next unless shard.database_server
386
+ close_connections_if_needed.call(shard)
387
+ shard.activate(*categories) do
388
+ begin
389
+ result.concat Array.wrap(yield)
390
+ rescue
391
+ case options[:exception]
392
+ when :ignore
393
+ when :defer
394
+ exception ||= $!
395
+ when Proc
396
+ options[:exception].call
397
+ when :raise
398
+ raise
399
+ else
400
+ raise
401
+ end
402
+ end
403
+ end
404
+ previous_shard = shard
405
+ end
406
+ close_connections_if_needed.call(Shard.current)
407
+ raise exception if exception
408
+ result
409
+ end
410
+
411
+ def partition_by_shard(array, partition_proc = nil)
412
+ shard_arrays = {}
413
+ array.each do |object|
414
+ partition_object = partition_proc ? partition_proc.call(object) : object
415
+ case partition_object
416
+ when Shard
417
+ shard = partition_object
418
+ when ::ActiveRecord::Base
419
+ if partition_object.respond_to?(:associated_shards)
420
+ partition_object.associated_shards.each do |a_shard|
421
+ shard_arrays[a_shard] ||= []
422
+ shard_arrays[a_shard] << object
423
+ end
424
+ next
425
+ else
426
+ shard = partition_object.shard
427
+ end
428
+ when Integer, /^\d+$/, /^(\d+)~(\d+)$/
429
+ local_id, shard = Shard.local_id_for(partition_object)
430
+ local_id ||= partition_object
431
+ object = local_id if !partition_proc
432
+ end
433
+ shard ||= Shard.current
434
+ shard_arrays[shard] ||= []
435
+ shard_arrays[shard] << object
436
+ end
437
+ # TODO: use with_each_shard (or vice versa) to get
438
+ # connection management and parallelism benefits
439
+ shard_arrays.inject([]) do |results, (shard, objects)|
440
+ results.concat shard.activate { Array.wrap(yield objects) }
441
+ end
442
+ end
443
+
444
+ # it's tedious to hold onto this same
445
+ # kind of sign state and transform the
446
+ # result in multiple places, so
447
+ # here we can operate on the absolute value
448
+ # in a provided block and trust the sign will
449
+ # stay as provided. This assumes no consumer
450
+ # will return a nil value from the block.
451
+ def signed_id_operation(input_id)
452
+ sign = input_id < 0 ? -1 : 1
453
+ output = yield input_id.abs
454
+ output * sign
455
+ end
456
+
457
+ # converts an AR object, integral id, string id, or string short-global-id to a
458
+ # integral id. nil if it can't be interpreted
459
+ def integral_id_for(any_id)
460
+ if any_id.is_a?(::Arel::Nodes::Casted)
461
+ any_id = any_id.val
462
+ elsif any_id.is_a?(::Arel::Nodes::BindParam) && ::Rails.version >= "5.2"
463
+ any_id = any_id.value.value_before_type_cast
464
+ end
465
+
466
+ case any_id
467
+ when ::ActiveRecord::Base
468
+ any_id.id
469
+ when /^(\d+)~(-?\d+)$/
470
+ local_id = $2.to_i
471
+ signed_id_operation(local_id) do |id|
472
+ return nil if id > IDS_PER_SHARD
473
+ $1.to_i * IDS_PER_SHARD + id
474
+ end
475
+ when Integer, /^-?\d+$/
476
+ any_id.to_i
477
+ else
478
+ nil
479
+ end
480
+ end
481
+
482
+ # takes an id-ish, and returns a local id and the shard it's
483
+ # local to. [nil, nil] if it can't be interpreted. [id, nil]
484
+ # if it's already a local ID. [nil, nil] if it's a well formed
485
+ # id, but the shard it refers to does not exist
486
+ NIL_NIL_ID = [nil, nil].freeze
487
+ def local_id_for(any_id)
488
+ id = integral_id_for(any_id)
489
+ return NIL_NIL_ID unless id
490
+ return_shard = nil
491
+ local_id = signed_id_operation(id) do |abs_id|
492
+ if abs_id < IDS_PER_SHARD
493
+ abs_id
494
+ elsif return_shard = lookup(abs_id / IDS_PER_SHARD)
495
+ abs_id % IDS_PER_SHARD
496
+ else
497
+ return NIL_NIL_ID
498
+ end
499
+ end
500
+ [local_id, return_shard]
501
+ end
502
+
503
+ # takes an id-ish, and returns an integral id relative to
504
+ # target_shard. returns nil if it can't be interpreted,
505
+ # or the integral version of the id if it refers to a shard
506
+ # that does not exist
507
+ def relative_id_for(any_id, source_shard, target_shard)
508
+ integral_id = integral_id_for(any_id)
509
+ local_id, shard = local_id_for(integral_id)
510
+ return integral_id unless local_id
511
+ shard ||= source_shard
512
+ return local_id if shard == target_shard
513
+ shard.global_id_for(local_id)
514
+ end
515
+
516
+ # takes an id-ish, and returns a shortened global
517
+ # string id if global, and itself if local.
518
+ # returns any_id itself if it can't be interpreted
519
+ def short_id_for(any_id)
520
+ local_id, shard = local_id_for(any_id)
521
+ return any_id unless local_id
522
+ return local_id unless shard
523
+ "#{shard.id}~#{local_id}"
524
+ end
525
+
526
+ # takes an id-ish, and returns an integral global id.
527
+ # returns nil if it can't be interpreted
528
+ def global_id_for(any_id, source_shard = nil)
529
+ id = integral_id_for(any_id)
530
+ return any_id unless id
531
+ signed_id_operation(id) do |abs_id|
532
+ if abs_id >= IDS_PER_SHARD
533
+ abs_id
534
+ else
535
+ source_shard ||= Shard.current
536
+ source_shard.global_id_for(abs_id)
537
+ end
538
+ end
539
+ end
540
+
541
+ def shard_for(any_id, source_shard = nil)
542
+ return any_id.shard if any_id.is_a?(::ActiveRecord::Base)
543
+ _, shard = local_id_for(any_id)
544
+ shard || source_shard || Shard.current
545
+ end
546
+
547
+ # given the provided option, determines whether we need to (and whether
548
+ # it's possible) to determine a reasonable default.
549
+ def determine_max_procs(max_procs_input, parallel_input=2)
550
+ max_procs = nil
551
+ if max_procs_input
552
+ max_procs = max_procs_input.to_i
553
+ max_procs = nil if max_procs == 0
554
+ else
555
+ return 1 if parallel_input.nil? || parallel_input < 1
556
+ cpus = Environment.cpu_count
557
+ if cpus && cpus > 0
558
+ max_procs = cpus * parallel_input
559
+ end
560
+ end
561
+
562
+ return max_procs
563
+ end
564
+
565
+ private
566
+ # in-process caching
567
+ def cached_shards
568
+ @cached_shards ||= {}.compare_by_identity
569
+ end
570
+
571
+ def add_to_cache(shard)
572
+ cached_shards[shard.id] = shard
573
+ end
574
+
575
+ def remove_from_cache(shard)
576
+ cached_shards.delete(shard.id)
577
+ end
578
+
579
+ def find_cached(key)
580
+ # can't simply cache the AR object since Shard has a custom serializer
581
+ # that calls this method
582
+ attributes = Switchman.cache.fetch(key) { yield&.attributes }
583
+ return nil unless attributes
584
+
585
+ shard = Shard.new
586
+ attributes.each do |attr, value|
587
+ shard.send(:"#{attr}=", value) if shard.respond_to?(:"#{attr}=")
588
+ end
589
+ shard.clear_changes_information
590
+ shard.instance_variable_set(:@new_record, false)
591
+ # connection info doesn't exist in database.yml;
592
+ # pretend the shard doesn't exist either
593
+ shard = nil unless shard.database_server
594
+ shard
595
+ end
596
+
597
+ def active_shards
598
+ Thread.current[:active_shards] ||= {}.compare_by_identity
599
+ end
600
+ end
601
+
602
+ def name
603
+ unless instance_variable_defined?(:@name)
604
+ # protect against re-entrancy
605
+ @name = nil
606
+ @name = read_attribute(:name) || default_name
607
+ end
608
+ @name
609
+ end
610
+
611
+ def name=(name)
612
+ write_attribute(:name, @name = name)
613
+ remove_instance_variable(:@name) if name == nil
614
+ end
615
+
616
+ def database_server
617
+ @database_server ||= DatabaseServer.find(self.database_server_id)
618
+ end
619
+
620
+ def database_server=(database_server)
621
+ self.database_server_id = database_server.id
622
+ @database_server = database_server
623
+ end
624
+
625
+ def primary?
626
+ self == database_server.primary_shard
627
+ end
628
+
629
+ def description
630
+ [database_server.id, name].compact.join(':')
631
+ end
632
+
633
+ # Shards are always on the default shard
634
+ def shard
635
+ Shard.default
636
+ end
637
+
638
+ def activate(*categories)
639
+ shards = hashify_categories(categories)
640
+ Shard.activate(shards) do
641
+ yield
642
+ end
643
+ end
644
+
645
+ # for use from console ONLY
646
+ def activate!(*categories)
647
+ shards = hashify_categories(categories)
648
+ Shard.activate!(shards)
649
+ nil
650
+ end
651
+
652
+ # custom serialization, since shard is self-referential
653
+ def _dump(depth)
654
+ self.id.to_s
655
+ end
656
+
657
+ def self._load(str)
658
+ lookup(str.to_i)
659
+ end
660
+
661
+ def drop_database
662
+ raise("Cannot drop the database of the default shard") if self.default?
663
+ return unless read_attribute(:name)
664
+
665
+ begin
666
+ adapter = self.database_server.config[:adapter]
667
+ sharding_config = Switchman.config || {}
668
+ drop_statement = sharding_config[adapter]&.[](:drop_statement)
669
+ drop_statement ||= sharding_config[:drop_statement]
670
+ if drop_statement
671
+ drop_statement = Array(drop_statement).dup.
672
+ map { |statement| statement.gsub('%{name}', self.name) }
673
+ end
674
+
675
+ case adapter
676
+ when 'mysql', 'mysql2'
677
+ self.activate do
678
+ ::GuardRail.activate(:deploy) do
679
+ drop_statement ||= "DROP DATABASE #{self.name}"
680
+ Array(drop_statement).each do |stmt|
681
+ ::ActiveRecord::Base.connection.execute(stmt)
682
+ end
683
+ end
684
+ end
685
+ when 'postgresql'
686
+ self.activate do
687
+ ::GuardRail.activate(:deploy) do
688
+ # Shut up, Postgres!
689
+ conn = ::ActiveRecord::Base.connection
690
+ old_proc = conn.raw_connection.set_notice_processor {}
691
+ begin
692
+ drop_statement ||= "DROP SCHEMA #{self.name} CASCADE"
693
+ Array(drop_statement).each do |stmt|
694
+ ::ActiveRecord::Base.connection.execute(stmt)
695
+ end
696
+ ensure
697
+ conn.raw_connection.set_notice_processor(&old_proc) if old_proc
698
+ end
699
+ end
700
+ end
701
+ end
702
+ rescue
703
+ logger.info "Drop failed: #{$!}"
704
+ end
705
+ end
706
+
707
+ # takes an id local to this shard, and returns a global id
708
+ def global_id_for(local_id)
709
+ return nil unless local_id
710
+ self.class.signed_id_operation(local_id) do |abs_id|
711
+ abs_id + self.id * IDS_PER_SHARD
712
+ end
713
+ end
714
+
715
+ # skip global_id.hash
716
+ def hash
717
+ id.hash
718
+ end
719
+
720
+ def destroy
721
+ raise("Cannot destroy the default shard") if self.default?
722
+ super
723
+ end
724
+
725
+ private
726
+
727
+ def clear_cache
728
+ Shard.default.activate do
729
+ Switchman.cache.delete(['shard', id].join('/'))
730
+ Switchman.cache.delete("default_shard") if default?
731
+ end
732
+ self.class.clear_cache
733
+ end
734
+
735
+ def default_name
736
+ database_server.shard_name(self)
737
+ end
738
+
739
+ def on_rollback
740
+ # make sure all connection pool proxies are referencing valid pools
741
+ ::ActiveRecord::Base.connection_handler.connection_pools.each do |pool|
742
+ next unless pool.is_a?(ConnectionPoolProxy)
743
+
744
+ pool.remove_shard!(self)
745
+ end
746
+ end
747
+
748
+ def hashify_categories(categories)
749
+ if categories.empty?
750
+ { :primary => self }
751
+ else
752
+ categories.inject({}) { |h, category| h[category] = self; h }
753
+ end
754
+ end
755
+
756
+ end
757
+ end