switchman 3.3.6 → 4.2.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +15 -14
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
  4. data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
  5. data/lib/switchman/active_record/abstract_adapter.rb +10 -6
  6. data/lib/switchman/active_record/associations.rb +72 -49
  7. data/lib/switchman/active_record/attribute_methods.rb +89 -44
  8. data/lib/switchman/active_record/base.rb +109 -40
  9. data/lib/switchman/active_record/calculations.rb +90 -54
  10. data/lib/switchman/active_record/connection_handler.rb +18 -0
  11. data/lib/switchman/active_record/connection_pool.rb +41 -23
  12. data/lib/switchman/active_record/database_configurations.rb +23 -13
  13. data/lib/switchman/active_record/finder_methods.rb +20 -14
  14. data/lib/switchman/active_record/log_subscriber.rb +3 -6
  15. data/lib/switchman/active_record/migration.rb +35 -12
  16. data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
  17. data/lib/switchman/active_record/persistence.rb +30 -0
  18. data/lib/switchman/active_record/postgresql_adapter.rb +37 -22
  19. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  20. data/lib/switchman/active_record/query_cache.rb +26 -17
  21. data/lib/switchman/active_record/query_methods.rb +148 -44
  22. data/lib/switchman/active_record/reflection.rb +9 -2
  23. data/lib/switchman/active_record/relation.rb +87 -17
  24. data/lib/switchman/active_record/spawn_methods.rb +3 -7
  25. data/lib/switchman/active_record/statement_cache.rb +4 -4
  26. data/lib/switchman/active_record/table_definition.rb +1 -1
  27. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  28. data/lib/switchman/active_record/test_fixtures.rb +71 -25
  29. data/lib/switchman/active_support/cache.rb +9 -4
  30. data/lib/switchman/arel.rb +16 -25
  31. data/lib/switchman/call_super.rb +2 -8
  32. data/lib/switchman/database_server.rb +67 -48
  33. data/lib/switchman/default_shard.rb +14 -3
  34. data/lib/switchman/engine.rb +35 -23
  35. data/lib/switchman/environment.rb +2 -2
  36. data/lib/switchman/errors.rb +13 -0
  37. data/lib/switchman/guard_rail/relation.rb +1 -2
  38. data/lib/switchman/parallel.rb +6 -6
  39. data/lib/switchman/r_spec_helper.rb +12 -11
  40. data/lib/switchman/shard.rb +168 -68
  41. data/lib/switchman/sharded_instrumenter.rb +9 -3
  42. data/lib/switchman/standard_error.rb +4 -0
  43. data/lib/switchman/test_helper.rb +3 -3
  44. data/lib/switchman/version.rb +1 -1
  45. data/lib/switchman.rb +27 -15
  46. data/lib/tasks/switchman.rake +96 -60
  47. metadata +28 -173
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'parallel'
3
+ require "parallel"
4
4
 
5
5
  module Switchman
6
6
  module Parallel
@@ -50,19 +50,19 @@ module Switchman
50
50
  end
51
51
  end
52
52
 
53
- class PrefixingIO
53
+ class TransformingIO
54
54
  delegate_missing_to :@original_io
55
55
 
56
- def initialize(prefix, original_io)
57
- @prefix = prefix
56
+ def initialize(transformer, original_io)
57
+ @transformer = transformer
58
58
  @original_io = original_io
59
59
  end
60
60
 
61
61
  def puts(*args)
62
- args.flatten.each { |arg| @original_io.puts "#{@prefix}: #{arg}" }
62
+ args.flatten.each { |arg| @original_io.puts @transformer.call(arg) }
63
63
  end
64
64
  end
65
65
  end
66
66
  end
67
67
 
68
- ::Parallel::UndumpableException.prepend(::Switchman::Parallel::UndumpableException)
68
+ Parallel::UndumpableException.prepend(Switchman::Parallel::UndumpableException)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'switchman/test_helper'
3
+ require "switchman/test_helper"
4
4
 
5
5
  module Switchman
6
6
  # including this module in your specs will give you several shards to
@@ -34,9 +34,9 @@ module Switchman
34
34
  groups = group.class.descendant_filtered_examples.map(&:example_group).uniq
35
35
  next unless groups.any? { |descendant_group| RSpecHelper.included_in?(descendant_group) }
36
36
 
37
- puts 'Setting up sharding for all specs...'
37
+ puts "Setting up sharding for all specs..."
38
38
  Shard.delete_all
39
- Switchman.cache.delete('default_shard')
39
+ Switchman.cache.delete("default_shard")
40
40
 
41
41
  @@shard1, @@shard2 = TestHelper.recreate_persistent_test_shards
42
42
  @@default_shard = Shard.default
@@ -48,7 +48,7 @@ module Switchman
48
48
  @@shard1 = @@shard1.create_new_shard
49
49
  @@shard2 = @@shard2.create_new_shard
50
50
  rescue => e
51
- warn 'Sharding setup FAILED!:'
51
+ warn "Sharding setup FAILED!:"
52
52
  while e
53
53
  warn "\n#{e}\n"
54
54
  warn e.backtrace
@@ -66,9 +66,9 @@ module Switchman
66
66
  # we'll re-persist in the group's `before :all`; we don't want them to exist
67
67
  # in the db before then
68
68
  Shard.delete_all
69
- Switchman.cache.delete('default_shard')
69
+ Switchman.cache.delete("default_shard")
70
70
  Shard.default(reload: true)
71
- puts 'Done!'
71
+ puts "Done!"
72
72
 
73
73
  main_pid = Process.pid
74
74
  at_exit do
@@ -76,7 +76,7 @@ module Switchman
76
76
 
77
77
  # preserve rspec's exit status
78
78
  status = $!.is_a?(::SystemExit) ? $!.status : nil
79
- puts 'Tearing down sharding for all specs'
79
+ puts "Tearing down sharding for all specs"
80
80
  @@shard1.database_server.destroy unless @@shard1.database_server == Shard.default.database_server
81
81
  unless @@keep_the_shards
82
82
  @@shard1.drop_database
@@ -95,7 +95,7 @@ module Switchman
95
95
  dup = @@default_shard.dup
96
96
  dup.id = @@default_shard.id
97
97
  dup.save!
98
- Switchman.cache.delete('default_shard')
98
+ Switchman.cache.delete("default_shard")
99
99
  Shard.default(reload: true)
100
100
  dup = @@shard1.dup
101
101
  dup.id = @@shard1.id
@@ -107,7 +107,7 @@ module Switchman
107
107
  end
108
108
 
109
109
  klass.before do
110
- raise 'Sharding did not set up correctly' if @@sharding_failed
110
+ raise "Sharding did not set up correctly" if @@sharding_failed
111
111
 
112
112
  Shard.clear_cache
113
113
  if use_transactional_tests
@@ -121,18 +121,19 @@ module Switchman
121
121
  next if @@sharding_failed
122
122
 
123
123
  # clean up after specs
124
- DatabaseServer.all.each do |ds|
124
+ DatabaseServer.each do |ds|
125
125
  if ds.fake? && ds != @shard2.database_server
126
126
  ds.shards.delete_all unless use_transactional_tests
127
127
  ds.destroy
128
128
  end
129
+ ds.remove_instance_variable(:@primary_shard_id) if ds.instance_variable_defined?(:@primary_shard_id)
129
130
  end
130
131
  end
131
132
 
132
133
  klass.after(:all) do
133
134
  # Don't truncate because that can create some fun cross-connection lock contention
134
135
  Shard.delete_all
135
- Switchman.cache.delete('default_shard')
136
+ Switchman.cache.delete("default_shard")
136
137
  Shard.default(reload: true)
137
138
  end
138
139
  end
@@ -5,14 +5,57 @@ module Switchman
5
5
  # ten trillion possible ids per shard. yup.
6
6
  IDS_PER_SHARD = 10_000_000_000_000
7
7
 
8
+ # rubocop:disable Style/SymbolProc -- transforming to a lambda produces "no receiver given"
8
9
  # only allow one default
9
10
  validates_uniqueness_of :default, if: ->(s) { s.default? }
11
+ # rubocop:enable Style/SymbolProc
10
12
 
11
13
  after_save :clear_cache
12
14
  after_destroy :clear_cache
13
15
 
14
16
  scope :primary, -> { where(name: nil).order(:database_server_id, :id).distinct_on(:database_server_id) }
15
17
 
18
+ scope :in_region, (lambda do |region, include_regionless: true|
19
+ next in_current_region if region.nil?
20
+
21
+ dbs_by_region = DatabaseServer.group_by(&:region)
22
+ db_count_in_this_region = dbs_by_region[region]&.length.to_i
23
+ db_count_in_this_region += dbs_by_region[nil]&.length.to_i if include_regionless
24
+ non_existent_database_servers = Shard.send(:non_existent_database_servers)
25
+ db_count_in_other_regions = DatabaseServer.all.length -
26
+ db_count_in_this_region +
27
+ non_existent_database_servers.length
28
+
29
+ dbs_in_this_region = dbs_by_region[region]&.map(&:id) || []
30
+ dbs_in_this_region += dbs_by_region[nil]&.map(&:id) || [] if include_regionless
31
+
32
+ if db_count_in_this_region <= db_count_in_other_regions
33
+ if dbs_in_this_region.include?(Shard.default.database_server.id)
34
+ where("database_server_id IN (?) OR database_server_id IS NULL", dbs_in_this_region)
35
+ else
36
+ where(database_server_id: dbs_in_this_region)
37
+ end
38
+ elsif db_count_in_other_regions.zero?
39
+ all
40
+ else
41
+ dbs_not_in_this_region = DatabaseServer.map(&:id) - dbs_in_this_region + non_existent_database_servers
42
+ if dbs_in_this_region.include?(Shard.default.database_server.id)
43
+ where("database_server_id NOT IN (?) OR database_server_id IS NULL", dbs_not_in_this_region)
44
+ else
45
+ where.not(database_server_id: dbs_not_in_this_region)
46
+ end
47
+ end
48
+ end)
49
+
50
+ scope :in_current_region, (lambda do |include_regionless: true|
51
+ # sharding isn't set up? maybe we're in tests, or a somehow degraded environment
52
+ # either way there's only one shard, and we always want to see it
53
+ return [default] unless default.is_a?(Switchman::Shard)
54
+ return all if !Switchman.region || DatabaseServer.none?(&:region)
55
+
56
+ in_region(Switchman.region, include_regionless:)
57
+ end)
58
+
16
59
  class << self
17
60
  def sharded_models
18
61
  @sharded_models ||= [::ActiveRecord::Base, UnshardedRecord].freeze
@@ -36,13 +79,15 @@ module Switchman
36
79
 
37
80
  # Now find the actual record, if it exists
38
81
  @default = begin
39
- find_cached('default_shard') { Shard.where(default: true).take } || default
82
+ find_cached("default_shard") { Shard.where(default: true).take } || default
40
83
  rescue
41
84
  default
42
85
  end
43
86
 
44
87
  # make sure this is not erroneously cached
45
- @default.database_server.remove_instance_variable(:@primary_shard) if @default.database_server.instance_variable_defined?(:@primary_shard)
88
+ if @default.database_server.instance_variable_defined?(:@primary_shard)
89
+ @default.database_server.remove_instance_variable(:@primary_shard)
90
+ end
46
91
 
47
92
  # and finally, check for cached references to the default shard on the existing connection
48
93
  sharded_models.each do |klass|
@@ -75,28 +120,30 @@ module Switchman
75
120
  klass.current_switchman_shard != shard
76
121
 
77
122
  (activated_classes ||= []) << klass
78
- klass.connected_to_stack << { shard: shard.database_server.id.to_sym, klasses: [klass], switchman_shard: shard }
123
+ klass.connected_to_stack << { shard: shard.database_server.id.to_sym,
124
+ klasses: [klass],
125
+ switchman_shard: shard }
79
126
  end
80
127
  activated_classes
81
128
  end
82
129
 
83
130
  def active_shards
84
- sharded_models.map do |klass|
131
+ sharded_models.filter_map do |klass|
85
132
  [klass, current(klass)]
86
- end.compact.to_h
133
+ end.to_h
87
134
  end
88
135
 
89
136
  def lookup(id)
90
137
  id_i = id.to_i
91
- return current if id_i == current.id || id == 'self'
92
- return default if id_i == default.id || id.nil? || id == 'default'
138
+ return current if id_i == current.id || id == "self"
139
+ return default if id_i == default.id || id.nil? || id == "default"
93
140
 
94
141
  id = id_i
95
142
  raise ArgumentError if id.zero?
96
143
 
97
144
  unless cached_shards.key?(id)
98
145
  cached_shards[id] = Shard.default.activate do
99
- find_cached(['shard', id]) { find_by(id: id) }
146
+ find_cached(["shard", id]) { find_by(id:) }
100
147
  end
101
148
  end
102
149
  cached_shards[id]
@@ -120,7 +167,7 @@ module Switchman
120
167
  # forking.
121
168
  # exception: - :ignore, :raise, :defer (wait until the end and raise the first
122
169
  # error), or a proc
123
- # output: - :simple, :decorated (with database_server_id:shard_name)
170
+ # output: - :simple, :decorated (with database_server_id:shard_name), custom lambda transformer
124
171
  def with_each_shard(*args, parallel: false, exception: :raise, output: :simple)
125
172
  raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
126
173
 
@@ -136,19 +183,28 @@ module Switchman
136
183
  scope, classes = args
137
184
  end
138
185
 
186
+ output = if output == :decorated
187
+ ->(arg) { "#{Shard.current.description}: #{arg}" }
188
+ elsif output == :simple
189
+ nil
190
+ else
191
+ output
192
+ end
139
193
  parallel = [Environment.cpu_count || 2, 2].min if parallel == true
140
194
  parallel = 0 if parallel == false || parallel.nil?
141
195
 
142
196
  scope ||= Shard.all
143
- scope = scope.order(::Arel.sql('database_server_id IS NOT NULL, database_server_id, id')) if ::ActiveRecord::Relation === scope && scope.order_values.empty?
197
+ if ::ActiveRecord::Relation === scope && scope.order_values.empty?
198
+ scope = scope.order(::Arel.sql("database_server_id IS NOT NULL, database_server_id, id"))
199
+ end
144
200
 
145
201
  if parallel > 1
146
202
  if ::ActiveRecord::Relation === scope
147
203
  # still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
148
- database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
149
- map(&:database_server).compact.uniq
204
+ database_servers = scope.reorder("database_server_id").select(:database_server_id).distinct
205
+ .filter_map(&:database_server).uniq
150
206
  # nothing to do
151
- return if database_servers.count.zero?
207
+ return if database_servers.none?
152
208
 
153
209
  scopes = database_servers.to_h do |server|
154
210
  [server, scope.merge(server.shards)]
@@ -160,20 +216,23 @@ module Switchman
160
216
  # clear connections prior to forking (no more queries will be executed in the parent,
161
217
  # and we want them gone so that we don't accidentally use them post-fork doing something
162
218
  # silly like dealloc'ing prepared statements)
163
- ::ActiveRecord::Base.clear_all_connections!
219
+ ::ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
164
220
 
165
- parent_process_name = `ps -ocommand= -p#{Process.pid}`.slice(/#{$0}.*/)
166
- ret = ::Parallel.map(scopes, in_processes: scopes.length > 1 ? parallel : 0) do |server, subscope|
221
+ parent_process_name = sanitized_process_title
222
+ ret = ::Parallel.map(scopes, in_processes: (scopes.length > 1) ? parallel : 0) do |server, subscope|
167
223
  name = server.id
168
224
  last_description = name
169
225
 
170
226
  begin
171
227
  max_length = 128 - name.length - 3
172
228
  short_parent_name = parent_process_name[0..max_length] if max_length >= 0
173
- new_title = [short_parent_name, name].join(' ')
229
+ new_title = [short_parent_name, name].join(" ")
174
230
  Process.setproctitle(new_title)
175
231
  Switchman.config[:on_fork_proc]&.call
176
- with_each_shard(subscope, classes, exception: exception, output: :decorated) do
232
+ with_each_shard(subscope,
233
+ classes,
234
+ exception:,
235
+ output: output || :decorated) do
177
236
  last_description = Shard.current.description
178
237
  Parallel::ResultWrapper.new(yield)
179
238
  end
@@ -187,8 +246,9 @@ module Switchman
187
246
  unless errors.empty?
188
247
  raise errors.first.exception if errors.length == 1
189
248
 
249
+ errors_desc = errors.map(&:name).sort.join(", ")
190
250
  raise Errors::ParallelShardExecError,
191
- "The following database server(s) did not finish processing cleanly: #{errors.map(&:name).sort.join(', ')}",
251
+ "The following database server(s) did not finish processing cleanly: #{errors_desc}",
192
252
  cause: errors.first.exception
193
253
  end
194
254
 
@@ -206,9 +266,9 @@ module Switchman
206
266
  next unless shard.database_server
207
267
 
208
268
  shard.activate(*classes) do
209
- if output == :decorated
210
- $stdout = Parallel::PrefixingIO.new(shard.description, $stdout)
211
- $stderr = Parallel::PrefixingIO.new(shard.description, $stderr)
269
+ if output
270
+ $stdout = Parallel::TransformingIO.new(output, $stdout)
271
+ $stderr = Parallel::TransformingIO.new(output, $stderr)
212
272
  end
213
273
 
214
274
  result.concat Array.wrap(yield)
@@ -251,7 +311,7 @@ module Switchman
251
311
  else
252
312
  shard = partition_object.shard
253
313
  end
254
- when Integer, /^\d+$/, /^(\d+)~(\d+)$/
314
+ when Integer, /\A\d+\Z/, /\A(\d+)~(\d+)\Z/
255
315
  local_id, shard = Shard.local_id_for(partition_object)
256
316
  local_id ||= partition_object
257
317
  object = local_id unless partition_proc
@@ -293,14 +353,14 @@ module Switchman
293
353
  case any_id
294
354
  when ::ActiveRecord::Base
295
355
  any_id.id
296
- when /^(\d+)~(-?\d+)$/
356
+ when /\A(\d+)~(-?\d+)\Z/
297
357
  local_id = $2.to_i
298
358
  signed_id_operation(local_id) do |id|
299
359
  return nil if id > IDS_PER_SHARD
300
360
 
301
361
  ($1.to_i * IDS_PER_SHARD) + id
302
362
  end
303
- when Integer, /^-?\d+$/
363
+ when Integer, /\A-?\d+\Z/
304
364
  any_id.to_i
305
365
  end
306
366
  end
@@ -384,7 +444,7 @@ module Switchman
384
444
  end
385
445
 
386
446
  def configure_connects_to
387
- full_connects_to_hash = DatabaseServer.all.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
447
+ full_connects_to_hash = DatabaseServer.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
388
448
  sharded_models.each do |klass|
389
449
  connects_to_hash = full_connects_to_hash.deep_dup
390
450
  if klass == UnshardedRecord
@@ -397,13 +457,16 @@ module Switchman
397
457
  connects_to_hash.each do |(db_name, role_hash)|
398
458
  role_hash.each_key do |role|
399
459
  role_hash.delete(role) if klass.connection_handler.retrieve_connection_pool(
400
- klass.connection_specification_name, role: role, shard: db_name
460
+ klass.connection_specification_name, role:, shard: db_name
401
461
  )
402
462
  end
403
463
  end
404
464
  end
405
465
 
466
+ # this resets the default shard on rails 7.1+, but we want to preserve it
467
+ shard_was = klass.default_shard
406
468
  klass.connects_to shards: connects_to_hash
469
+ klass.default_shard = shard_was
407
470
  end
408
471
  end
409
472
 
@@ -437,8 +500,50 @@ module Switchman
437
500
  shard = nil unless shard.database_server
438
501
  shard
439
502
  end
503
+
504
+ # Determines the name of the current process, including arguments, but stripping
505
+ # any shebang from the invoked script, and any additional path info from the
506
+ # executable.
507
+ #
508
+ # @return [String]
509
+ def sanitized_process_title
510
+ # get the effective process name from `ps`; this will include any changes
511
+ # from Process.setproctitle _or_ assigning to $0.
512
+ parent_process_name = `ps -ocommand= -p#{Process.pid}`.strip
513
+ # Effective process titles may be shorter than the actual
514
+ # command; truncate our ARGV[0] so that they are comparable
515
+ # for the next step
516
+ argv0 = if parent_process_name.length < Process.argv0.length
517
+ Process.argv0[0..parent_process_name.length]
518
+ else
519
+ Process.argv0
520
+ end
521
+
522
+ # when running via a shebang, the `ps` output will include the shebang
523
+ # (i.e. it will be "ruby bin/rails c"); attempt to strip it off.
524
+ # Note that argv0 in this case will _only_ be `bin/rails` (no shebang,
525
+ # no arguments). We want to preserve the arguments we got from `ps`
526
+ if (index = parent_process_name.index(argv0))
527
+ parent_process_name.slice!(0...index)
528
+ end
529
+
530
+ # remove directories from the main executable to make more room
531
+ # for additional info
532
+ argv = parent_process_name.shellsplit
533
+ argv[0] = File.basename(argv[0])
534
+ argv.shelljoin
535
+ end
536
+
537
+ # @return [Array<String>] the list of database servers that are in the
538
+ # config, but don't have any shards on them
539
+ def non_existent_database_servers
540
+ @non_existent_database_servers ||=
541
+ Shard.distinct.pluck(:database_server_id).compact - DatabaseServer.all.map(&:id)
542
+ end
440
543
  end
441
544
 
545
+ delegate :region, :in_region?, :in_current_region?, to: :database_server
546
+
442
547
  def name
443
548
  unless instance_variable_defined?(:@name)
444
549
  # protect against re-entrancy
@@ -467,7 +572,7 @@ module Switchman
467
572
  end
468
573
 
469
574
  def description
470
- [database_server.id, name].compact.join(':')
575
+ [database_server.id, name].compact.join(":")
471
576
  end
472
577
 
473
578
  # Shards are always on the default shard
@@ -479,9 +584,13 @@ module Switchman
479
584
  id
480
585
  end
481
586
 
482
- def activate(*classes, &block)
587
+ def original_id_value
588
+ id
589
+ end
590
+
591
+ def activate(*classes, &)
483
592
  shards = hashify_classes(classes)
484
- Shard.activate(shards, &block)
593
+ Shard.activate(shards, &)
485
594
  end
486
595
 
487
596
  # for use from console ONLY
@@ -501,48 +610,39 @@ module Switchman
501
610
  end
502
611
 
503
612
  def drop_database
504
- raise('Cannot drop the database of the default shard') if default?
613
+ raise "Cannot drop the database of the default shard" if default?
505
614
  return unless read_attribute(:name)
506
615
 
507
616
  begin
508
- adapter = database_server.config[:adapter]
509
- sharding_config = Switchman.config || {}
510
- drop_statement = sharding_config[adapter]&.[](:drop_statement)
511
- drop_statement ||= sharding_config[:drop_statement]
512
- if drop_statement
513
- drop_statement = Array(drop_statement).dup.
514
- map { |statement| statement.gsub('%{name}', name) }
617
+ activate do
618
+ self.class.drop_database(name)
515
619
  end
620
+ rescue ::ActiveRecord::StatementInvalid => e
621
+ logger.error "Drop failed: #{e}"
622
+ end
623
+ end
516
624
 
517
- case adapter
518
- when 'mysql', 'mysql2'
519
- activate do
520
- ::GuardRail.activate(:deploy) do
521
- drop_statement ||= "DROP DATABASE #{name}"
522
- Array(drop_statement).each do |stmt|
523
- ::ActiveRecord::Base.connection.execute(stmt)
524
- end
525
- end
526
- end
527
- when 'postgresql'
528
- activate do
529
- ::GuardRail.activate(:deploy) do
530
- # Shut up, Postgres!
531
- conn = ::ActiveRecord::Base.connection
532
- old_proc = conn.raw_connection.set_notice_processor {}
533
- begin
534
- drop_statement ||= "DROP SCHEMA #{name} CASCADE"
535
- Array(drop_statement).each do |stmt|
536
- ::ActiveRecord::Base.connection.execute(stmt)
537
- end
538
- ensure
539
- conn.raw_connection.set_notice_processor(&old_proc) if old_proc
540
- end
541
- end
625
+ #
626
+ # Drops a specific database/schema from the currently active connection
627
+ #
628
+ def self.drop_database(name)
629
+ sharding_config = Switchman.config || {}
630
+ drop_statement = sharding_config["postgresql"]&.[](:drop_statement)
631
+ drop_statement ||= sharding_config[:drop_statement]
632
+ drop_statement = Array(drop_statement).map { |statement| statement.gsub("%{name}", name) } if drop_statement
633
+
634
+ ::GuardRail.activate(:deploy) do
635
+ # Shut up, Postgres!
636
+ conn = ::ActiveRecord::Base.connection
637
+ old_proc = conn.raw_connection.set_notice_processor {}
638
+ begin
639
+ drop_statement ||= "DROP SCHEMA #{name} CASCADE"
640
+ Array(drop_statement).each do |stmt|
641
+ ::ActiveRecord::Base.connection.execute(stmt)
542
642
  end
643
+ ensure
644
+ conn.raw_connection.set_notice_processor(&old_proc) if old_proc
543
645
  end
544
- rescue
545
- logger.info "Drop failed: #{$!}"
546
646
  end
547
647
  end
548
648
 
@@ -561,7 +661,7 @@ module Switchman
561
661
  end
562
662
 
563
663
  def destroy
564
- raise('Cannot destroy the default shard') if default?
664
+ raise("Cannot destroy the default shard") if default?
565
665
 
566
666
  super
567
667
  end
@@ -570,8 +670,8 @@ module Switchman
570
670
 
571
671
  def clear_cache
572
672
  Shard.default.activate do
573
- Switchman.cache.delete(['shard', id].join('/'))
574
- Switchman.cache.delete('default_shard') if default?
673
+ Switchman.cache.delete(["shard", id].join("/"))
674
+ Switchman.cache.delete("default_shard") if default?
575
675
  end
576
676
  self.class.clear_cache
577
677
  end
@@ -3,7 +3,7 @@
3
3
  module Switchman
4
4
  class ShardedInstrumenter < ::SimpleDelegator
5
5
  def initialize(instrumenter, shard_host)
6
- super instrumenter
6
+ super(instrumenter)
7
7
  @shard_host = shard_host
8
8
  end
9
9
 
@@ -13,13 +13,19 @@ module Switchman
13
13
  # when we might be doing a query while defining attribute methods,
14
14
  # so just avoid logging then
15
15
  if shard.is_a?(Shard) && Shard.instance_variable_get(:@attribute_methods_generated)
16
+ env = if ::Rails.version < "8.0"
17
+ @shard_host.pool.connection_class&.current_role
18
+ else
19
+ @shard_host.pool.connection_descriptor&.name&.constantize&.current_role
20
+ end
21
+
16
22
  payload[:shard] = {
17
23
  database_server_id: shard.database_server.id,
18
24
  id: shard.id,
19
- env: ::Rails.version < '7.0' ? @shard_host.pool.connection_klass&.current_role : @shard_host.pool.connection_class&.current_role
25
+ env:
20
26
  }
21
27
  end
22
- super name, payload
28
+ super
23
29
  end
24
30
  end
25
31
  end
@@ -13,6 +13,10 @@ module Switchman
13
13
  Thread.current[:switchman_error_handler] = true
14
14
 
15
15
  @active_shards ||= Shard.active_shards
16
+ rescue
17
+ # intentionally empty - don't allow calculating the active_shards to prevent
18
+ # creating the StandardError for any reason. this prevents various random issues
19
+ # when a StandardError is created within a finalizer
16
20
  ensure
17
21
  Thread.current[:switchman_error_handler] = nil
18
22
  end
@@ -19,7 +19,7 @@ module Switchman
19
19
  end
20
20
 
21
21
  server1 = Shard.default.database_server
22
- server2 = DatabaseServer.create(Shard.default.database_server.config.merge(server2: true))
22
+ server2 = DatabaseServer.create(Shard.default.database_server.config.merge(server2: true, schema_dump: false))
23
23
 
24
24
  if server1 == Shard.default.database_server && server1.config[:shard1] && server1.config[:shard2]
25
25
  # look for the shards in the db already
@@ -63,9 +63,9 @@ module Switchman
63
63
 
64
64
  def find_existing_test_shard(server, name)
65
65
  if server == Shard.default.database_server
66
- server.shards.where(name: name).first
66
+ server.shards.where(name:).first
67
67
  else
68
- shard = Shard.where('database_server_id IS NOT NULL AND name=?', name).first
68
+ shard = Shard.where("database_server_id IS NOT NULL AND name=?", name).first
69
69
  # if somehow databases got created in a different order, change the shard to match
70
70
  shard.database_server = server if shard
71
71
  shard
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- VERSION = '3.3.6'
4
+ VERSION = "4.2.5"
5
5
  end