switchman 1.13.3 → 2.2.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/app/models/switchman/shard.rb +712 -11
  3. data/db/migrate/20130328212039_create_switchman_shards.rb +2 -0
  4. data/db/migrate/20130328224244_create_default_shard.rb +3 -1
  5. data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +2 -0
  6. data/db/migrate/20180828183945_add_default_shard_index.rb +3 -1
  7. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +2 -0
  8. data/db/migrate/20190114212900_add_unique_name_indexes.rb +2 -0
  9. data/lib/switchman/action_controller/caching.rb +2 -0
  10. data/lib/switchman/active_record/abstract_adapter.rb +6 -4
  11. data/lib/switchman/active_record/association.rb +45 -16
  12. data/lib/switchman/active_record/attribute_methods.rb +43 -17
  13. data/lib/switchman/active_record/base.rb +60 -20
  14. data/lib/switchman/active_record/batches.rb +2 -0
  15. data/lib/switchman/active_record/calculations.rb +5 -2
  16. data/lib/switchman/active_record/connection_handler.rb +48 -25
  17. data/lib/switchman/active_record/connection_pool.rb +19 -15
  18. data/lib/switchman/active_record/finder_methods.rb +2 -0
  19. data/lib/switchman/active_record/log_subscriber.rb +10 -12
  20. data/lib/switchman/active_record/migration.rb +48 -2
  21. data/lib/switchman/active_record/model_schema.rb +3 -1
  22. data/lib/switchman/active_record/persistence.rb +13 -2
  23. data/lib/switchman/active_record/postgresql_adapter.rb +149 -139
  24. data/lib/switchman/active_record/predicate_builder.rb +3 -1
  25. data/lib/switchman/active_record/query_cache.rb +19 -107
  26. data/lib/switchman/active_record/query_methods.rb +25 -3
  27. data/lib/switchman/active_record/reflection.rb +21 -8
  28. data/lib/switchman/active_record/relation.rb +66 -10
  29. data/lib/switchman/active_record/spawn_methods.rb +2 -0
  30. data/lib/switchman/active_record/statement_cache.rb +6 -25
  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 +2 -0
  34. data/lib/switchman/active_support/cache.rb +18 -0
  35. data/lib/switchman/arel.rb +2 -0
  36. data/lib/switchman/call_super.rb +2 -0
  37. data/lib/switchman/connection_pool_proxy.rb +44 -22
  38. data/lib/switchman/database_server.rb +34 -19
  39. data/lib/switchman/default_shard.rb +3 -0
  40. data/lib/switchman/engine.rb +20 -15
  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 +9 -7
  47. data/lib/switchman/rails.rb +2 -0
  48. data/lib/switchman/schema_cache.rb +11 -1
  49. data/lib/switchman/sharded_instrumenter.rb +3 -1
  50. data/lib/switchman/standard_error.rb +3 -1
  51. data/lib/switchman/test_helper.rb +7 -11
  52. data/lib/switchman/version.rb +3 -1
  53. data/lib/switchman.rb +5 -1
  54. data/lib/tasks/switchman.rake +24 -17
  55. metadata +53 -26
  56. data/app/models/switchman/shard_internal.rb +0 -714
@@ -1,11 +1,712 @@
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_dependency '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 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
+ def clear_cache
138
+ cached_shards.clear
139
+ end
140
+
141
+ # ==== Parameters
142
+ #
143
+ # * +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
151
+ # 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
154
+
155
+ unless default.is_a?(Shard)
156
+ return Array.wrap(yield)
157
+ end
158
+
159
+ options = args.extract_options!
160
+ if args.length == 1
161
+ if Array === args.first && args.first.first.is_a?(Symbol)
162
+ categories = args.first
163
+ else
164
+ scope = args.first
165
+ end
166
+ else
167
+ scope, categories = args
168
+ end
169
+
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)
182
+
183
+ 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
187
+
188
+ if parallel > 1
189
+ if ::ActiveRecord::Relation === scope
190
+ # still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
191
+ database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
192
+ map(&:database_server).compact.uniq
193
+ # nothing to do
194
+ return if database_servers.count == 0
195
+
196
+ scopes = Hash[database_servers.map do |server|
197
+ [server, server.shards.merge(scope)]
198
+ end]
199
+ else
200
+ scopes = scope.group_by(&:database_server)
201
+ end
202
+
203
+ exception_pipes = []
204
+ pids = []
205
+ out_fds = []
206
+ err_fds = []
207
+ pid_to_name_map = {}
208
+ fd_to_name_map = {}
209
+ errors = []
210
+
211
+ wait_for_output = lambda do |out_fds, err_fds, fd_to_name_map|
212
+ ready, _ = IO.select(out_fds + err_fds)
213
+ ready.each do |fd|
214
+ if fd.eof?
215
+ fd.close
216
+ out_fds.delete(fd)
217
+ err_fds.delete(fd)
218
+ next
219
+ end
220
+ line = fd.readline
221
+ puts "#{fd_to_name_map[fd]}: #{line}"
222
+ end
223
+ end
224
+
225
+ # only one process; don't bother forking
226
+ if scopes.length == 1
227
+ return with_each_shard(scopes.first.last, categories, options) { yield }
228
+ end
229
+
230
+ # clear connections prior to forking (no more queries will be executed in the parent,
231
+ # and we want them gone so that we don't accidentally use them post-fork doing something
232
+ # silly like dealloc'ing prepared statements)
233
+ ::ActiveRecord::Base.clear_all_connections!
234
+
235
+ scopes.each do |server, subscope|
236
+ name = server.id
237
+
238
+ exception_pipe = IO.pipe
239
+ exception_pipes << exception_pipe
240
+ pid, io_in, io_out, io_err = Open4.pfork4(lambda do
241
+ begin
242
+ Switchman.config[:on_fork_proc]&.call
243
+
244
+ # set a pretty name for the process title, up to 128 characters
245
+ # (we don't actually know the limit, depending on how the process
246
+ # was started)
247
+ # first, simplify the binary name by stripping directories,
248
+ # 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)
250
+ 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(" ")
256
+ Process.setproctitle(new_title)
257
+
258
+ with_each_shard(subscope, categories, options) { yield }
259
+ exception_pipe.last.close
260
+ rescue Exception => e
261
+ begin
262
+ dumped = Marshal.dump(e)
263
+ dumped = nil if dumped.length > 64 * 1024
264
+ rescue
265
+ dumped = nil
266
+ end
267
+
268
+ if dumped.nil?
269
+ # couldn't dump the exception; create a copy with just
270
+ # the message and the backtrace
271
+ 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)
278
+ e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
279
+ dumped = Marshal.dump(e2)
280
+ end
281
+
282
+ exception_pipe.last.set_encoding(dumped.encoding)
283
+ exception_pipe.last.write(dumped)
284
+ exception_pipe.last.flush
285
+ exception_pipe.last.close
286
+ exit! 1
287
+ 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
+ end
308
+ end
309
+
310
+ while out_fds.any? || err_fds.any?
311
+ wait_for_output.call(out_fds, err_fds, fd_to_name_map)
312
+ end
313
+ pids.each do |pid|
314
+ _, status = Process.waitpid2(pid)
315
+ errors << pid_to_name_map[pid] if status.exitstatus != 0
316
+ end
317
+
318
+ # check for an exception; we only re-raise the first one
319
+ 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
328
+ end
329
+
330
+ unless errors.empty?
331
+ raise ParallelShardExecError.new("The following subprocesses did not exit cleanly: #{errors.sort.join(", ")}")
332
+ end
333
+ return
334
+ end
335
+
336
+ categories ||= []
337
+
338
+ 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
+ result = []
355
+ exception = nil
356
+ scope.each do |shard|
357
+ # shard references a database server that isn't configured in this environment
358
+ 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
375
+ end
376
+ end
377
+ previous_shard = shard
378
+ end
379
+ close_connections_if_needed.call(Shard.current)
380
+ raise exception if exception
381
+ result
382
+ end
383
+
384
+ def partition_by_shard(array, partition_proc = nil)
385
+ shard_arrays = {}
386
+ array.each do |object|
387
+ partition_object = partition_proc ? partition_proc.call(object) : object
388
+ 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
400
+ 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
405
+ end
406
+ shard ||= Shard.current
407
+ shard_arrays[shard] ||= []
408
+ shard_arrays[shard] << object
409
+ end
410
+ # TODO: use with_each_shard (or vice versa) to get
411
+ # connection management and parallelism benefits
412
+ shard_arrays.inject([]) do |results, (shard, objects)|
413
+ results.concat shard.activate { Array.wrap(yield objects) }
414
+ end
415
+ end
416
+
417
+ # it's tedious to hold onto this same
418
+ # kind of sign state and transform the
419
+ # result in multiple places, so
420
+ # here we can operate on the absolute value
421
+ # in a provided block and trust the sign will
422
+ # stay as provided. This assumes no consumer
423
+ # will return a nil value from the block.
424
+ def signed_id_operation(input_id)
425
+ sign = input_id < 0 ? -1 : 1
426
+ output = yield input_id.abs
427
+ output * sign
428
+ end
429
+
430
+ # converts an AR object, integral id, string id, or string short-global-id to a
431
+ # integral id. nil if it can't be interpreted
432
+ 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"
436
+ any_id = any_id.value.value_before_type_cast
437
+ end
438
+
439
+ case any_id
440
+ when ::ActiveRecord::Base
441
+ any_id.id
442
+ when /^(\d+)~(-?\d+)$/
443
+ local_id = $2.to_i
444
+ signed_id_operation(local_id) do |id|
445
+ return nil if id > IDS_PER_SHARD
446
+ $1.to_i * IDS_PER_SHARD + id
447
+ end
448
+ when Integer, /^-?\d+$/
449
+ any_id.to_i
450
+ else
451
+ nil
452
+ end
453
+ end
454
+
455
+ # takes an id-ish, and returns a local id and the shard it's
456
+ # local to. [nil, nil] if it can't be interpreted. [id, nil]
457
+ # if it's already a local ID. [nil, nil] if it's a well formed
458
+ # id, but the shard it refers to does not exist
459
+ NIL_NIL_ID = [nil, nil].freeze
460
+ def local_id_for(any_id)
461
+ id = integral_id_for(any_id)
462
+ return NIL_NIL_ID unless id
463
+ return_shard = nil
464
+ local_id = signed_id_operation(id) do |abs_id|
465
+ if abs_id < IDS_PER_SHARD
466
+ abs_id
467
+ elsif return_shard = lookup(abs_id / IDS_PER_SHARD)
468
+ abs_id % IDS_PER_SHARD
469
+ else
470
+ return NIL_NIL_ID
471
+ end
472
+ end
473
+ [local_id, return_shard]
474
+ end
475
+
476
+ # takes an id-ish, and returns an integral id relative to
477
+ # target_shard. returns nil if it can't be interpreted,
478
+ # or the integral version of the id if it refers to a shard
479
+ # that does not exist
480
+ def relative_id_for(any_id, source_shard, target_shard)
481
+ integral_id = integral_id_for(any_id)
482
+ local_id, shard = local_id_for(integral_id)
483
+ return integral_id unless local_id
484
+ shard ||= source_shard
485
+ return local_id if shard == target_shard
486
+ shard.global_id_for(local_id)
487
+ end
488
+
489
+ # takes an id-ish, and returns a shortened global
490
+ # string id if global, and itself if local.
491
+ # returns any_id itself if it can't be interpreted
492
+ def short_id_for(any_id)
493
+ local_id, shard = local_id_for(any_id)
494
+ return any_id unless local_id
495
+ return local_id unless shard
496
+ "#{shard.id}~#{local_id}"
497
+ end
498
+
499
+ # takes an id-ish, and returns an integral global id.
500
+ # returns nil if it can't be interpreted
501
+ def global_id_for(any_id, source_shard = nil)
502
+ id = integral_id_for(any_id)
503
+ return any_id unless id
504
+ signed_id_operation(id) do |abs_id|
505
+ if abs_id >= IDS_PER_SHARD
506
+ abs_id
507
+ else
508
+ source_shard ||= Shard.current
509
+ source_shard.global_id_for(abs_id)
510
+ end
511
+ end
512
+ end
513
+
514
+ def shard_for(any_id, source_shard = nil)
515
+ return any_id.shard if any_id.is_a?(::ActiveRecord::Base)
516
+ _, shard = local_id_for(any_id)
517
+ shard || source_shard || Shard.current
518
+ end
519
+
520
+ private
521
+ # in-process caching
522
+ def cached_shards
523
+ @cached_shards ||= {}.compare_by_identity
524
+ end
525
+
526
+ def add_to_cache(shard)
527
+ cached_shards[shard.id] = shard
528
+ end
529
+
530
+ def remove_from_cache(shard)
531
+ cached_shards.delete(shard.id)
532
+ end
533
+
534
+ def find_cached(key)
535
+ # can't simply cache the AR object since Shard has a custom serializer
536
+ # that calls this method
537
+ attributes = Switchman.cache.fetch(key) { yield&.attributes }
538
+ return nil unless attributes
539
+
540
+ shard = Shard.new
541
+ attributes.each do |attr, value|
542
+ shard.send(:"#{attr}=", value) if shard.respond_to?(:"#{attr}=")
543
+ end
544
+ shard.clear_changes_information
545
+ shard.instance_variable_set(:@new_record, false)
546
+ # connection info doesn't exist in database.yml;
547
+ # pretend the shard doesn't exist either
548
+ shard = nil unless shard.database_server
549
+ shard
550
+ end
551
+
552
+ def active_shards
553
+ Thread.current[:active_shards] ||= {}.compare_by_identity
554
+ end
555
+ end
556
+
557
+ def name
558
+ unless instance_variable_defined?(:@name)
559
+ # protect against re-entrancy
560
+ @name = nil
561
+ @name = read_attribute(:name) || default_name
562
+ end
563
+ @name
564
+ end
565
+
566
+ def name=(name)
567
+ write_attribute(:name, @name = name)
568
+ remove_instance_variable(:@name) if name == nil
569
+ end
570
+
571
+ def database_server
572
+ @database_server ||= DatabaseServer.find(self.database_server_id)
573
+ end
574
+
575
+ def database_server=(database_server)
576
+ self.database_server_id = database_server.id
577
+ @database_server = database_server
578
+ end
579
+
580
+ def primary?
581
+ self == database_server.primary_shard
582
+ end
583
+
584
+ def description
585
+ [database_server.id, name].compact.join(':')
586
+ end
587
+
588
+ # Shards are always on the default shard
589
+ def shard
590
+ Shard.default
591
+ end
592
+
593
+ def activate(*categories)
594
+ shards = hashify_categories(categories)
595
+ Shard.activate(shards) do
596
+ yield
597
+ end
598
+ end
599
+
600
+ # for use from console ONLY
601
+ def activate!(*categories)
602
+ shards = hashify_categories(categories)
603
+ Shard.activate!(shards)
604
+ nil
605
+ end
606
+
607
+ # custom serialization, since shard is self-referential
608
+ def _dump(depth)
609
+ self.id.to_s
610
+ end
611
+
612
+ def self._load(str)
613
+ lookup(str.to_i)
614
+ end
615
+
616
+ def drop_database
617
+ raise("Cannot drop the database of the default shard") if self.default?
618
+ return unless read_attribute(:name)
619
+
620
+ begin
621
+ adapter = self.database_server.config[:adapter]
622
+ sharding_config = Switchman.config || {}
623
+ drop_statement = sharding_config[adapter]&.[](:drop_statement)
624
+ drop_statement ||= sharding_config[:drop_statement]
625
+ if drop_statement
626
+ drop_statement = Array(drop_statement).dup.
627
+ map { |statement| statement.gsub('%{name}', self.name) }
628
+ end
629
+
630
+ 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
638
+ end
639
+ 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
653
+ end
654
+ end
655
+ end
656
+ end
657
+ rescue
658
+ logger.info "Drop failed: #{$!}"
659
+ end
660
+ end
661
+
662
+ # takes an id local to this shard, and returns a global id
663
+ def global_id_for(local_id)
664
+ return nil unless local_id
665
+ self.class.signed_id_operation(local_id) do |abs_id|
666
+ abs_id + self.id * IDS_PER_SHARD
667
+ end
668
+ end
669
+
670
+ # skip global_id.hash
671
+ def hash
672
+ id.hash
673
+ end
674
+
675
+ def destroy
676
+ raise("Cannot destroy the default shard") if self.default?
677
+ super
678
+ end
679
+
680
+ private
681
+
682
+ def clear_cache
683
+ Shard.default.activate do
684
+ Switchman.cache.delete(['shard', id].join('/'))
685
+ Switchman.cache.delete("default_shard") if default?
686
+ end
687
+ self.class.clear_cache
688
+ end
689
+
690
+ def default_name
691
+ database_server.shard_name(self)
692
+ end
693
+
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 }
706
+ else
707
+ categories.inject({}) { |h, category| h[category] = self; h }
708
+ end
709
+ end
710
+
711
+ end
712
+ end