switchman 1.14.10 → 1.15.0

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