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