switchman 3.3.1 → 4.1.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 (48) 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 +5 -3
  6. data/lib/switchman/active_record/associations.rb +91 -51
  7. data/lib/switchman/active_record/attribute_methods.rb +88 -43
  8. data/lib/switchman/active_record/base.rb +113 -40
  9. data/lib/switchman/active_record/calculations.rb +98 -51
  10. data/lib/switchman/active_record/connection_handler.rb +18 -0
  11. data/lib/switchman/active_record/connection_pool.rb +56 -6
  12. data/lib/switchman/active_record/database_configurations.rb +37 -15
  13. data/lib/switchman/active_record/finder_methods.rb +47 -17
  14. data/lib/switchman/active_record/log_subscriber.rb +11 -5
  15. data/lib/switchman/active_record/migration.rb +51 -3
  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 +57 -20
  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 +79 -15
  24. data/lib/switchman/active_record/spawn_methods.rb +3 -7
  25. data/lib/switchman/active_record/statement_cache.rb +2 -2
  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 +75 -25
  29. data/lib/switchman/active_support/cache.rb +9 -4
  30. data/lib/switchman/arel.rb +34 -18
  31. data/lib/switchman/call_super.rb +2 -8
  32. data/lib/switchman/database_server.rb +72 -34
  33. data/lib/switchman/default_shard.rb +14 -3
  34. data/lib/switchman/engine.rb +38 -22
  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 +185 -71
  41. data/lib/switchman/sharded_instrumenter.rb +3 -3
  42. data/lib/switchman/shared_schema_cache.rb +11 -0
  43. data/lib/switchman/standard_error.rb +4 -0
  44. data/lib/switchman/test_helper.rb +3 -3
  45. data/lib/switchman/version.rb +1 -1
  46. data/lib/switchman.rb +27 -15
  47. data/lib/tasks/switchman.rake +96 -60
  48. metadata +50 -46
@@ -13,9 +13,8 @@ module Switchman
13
13
  end
14
14
 
15
15
  %w[update_all delete_all].each do |method|
16
- arg_params = RUBY_VERSION <= '2.8' ? '*args' : '*args, **kwargs'
17
16
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
18
- def #{method}(#{arg_params})
17
+ def #{method}(*args, **kwargs)
19
18
  db = Shard.current(connection_class_for_self).database_server
20
19
  db.unguard { super }
21
20
  end
@@ -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,8 @@ 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
- def with_each_shard(*args, parallel: false, exception: :raise, &block)
170
+ # output: - :simple, :decorated (with database_server_id:shard_name), custom lambda transformer
171
+ def with_each_shard(*args, parallel: false, exception: :raise, output: :simple)
124
172
  raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
125
173
 
126
174
  return Array.wrap(yield) unless default.is_a?(Shard)
@@ -135,17 +183,26 @@ module Switchman
135
183
  scope, classes = args
136
184
  end
137
185
 
186
+ output = if output == :decorated
187
+ ->(arg) { "#{Shard.current.description}: #{arg}" }
188
+ elsif output == :simple
189
+ nil
190
+ else
191
+ output
192
+ end
138
193
  parallel = [Environment.cpu_count || 2, 2].min if parallel == true
139
194
  parallel = 0 if parallel == false || parallel.nil?
140
195
 
141
196
  scope ||= Shard.all
142
- 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
143
200
 
144
201
  if parallel > 1
145
202
  if ::ActiveRecord::Relation === scope
146
203
  # still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
147
- database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
148
- map(&:database_server).compact.uniq
204
+ database_servers = scope.reorder("database_server_id").select(:database_server_id).distinct
205
+ .filter_map(&:database_server).uniq
149
206
  # nothing to do
150
207
  return if database_servers.count.zero?
151
208
 
@@ -159,25 +216,33 @@ module Switchman
159
216
  # clear connections prior to forking (no more queries will be executed in the parent,
160
217
  # and we want them gone so that we don't accidentally use them post-fork doing something
161
218
  # silly like dealloc'ing prepared statements)
162
- ::ActiveRecord::Base.clear_all_connections!
219
+ if ::Rails.version < "7.1"
220
+ ::ActiveRecord::Base.clear_all_connections!(nil)
221
+ else
222
+ ::ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
223
+ end
163
224
 
164
- parent_process_name = `ps -ocommand= -p#{Process.pid}`.slice(/#{$0}.*/)
165
- ret = ::Parallel.map(scopes, in_processes: scopes.length > 1 ? parallel : 0) do |server, subscope|
225
+ parent_process_name = sanitized_process_title
226
+ ret = ::Parallel.map(scopes, in_processes: (scopes.length > 1) ? parallel : 0) do |server, subscope|
166
227
  name = server.id
167
- # rubocop:disable Style/GlobalStdStream
168
- $stdout = Parallel::PrefixingIO.new(name, STDOUT)
169
- $stderr = Parallel::PrefixingIO.new(name, STDERR)
170
- # rubocop:enable Style/GlobalStdStream
228
+ last_description = name
229
+
171
230
  begin
172
231
  max_length = 128 - name.length - 3
173
232
  short_parent_name = parent_process_name[0..max_length] if max_length >= 0
174
- new_title = [short_parent_name, name].join(' ')
233
+ new_title = [short_parent_name, name].join(" ")
175
234
  Process.setproctitle(new_title)
176
235
  Switchman.config[:on_fork_proc]&.call
177
- with_each_shard(subscope, classes, exception: exception, &block).map { |result| Parallel::ResultWrapper.new(result) }
236
+ with_each_shard(subscope,
237
+ classes,
238
+ exception:,
239
+ output: output || :decorated) do
240
+ last_description = Shard.current.description
241
+ Parallel::ResultWrapper.new(yield)
242
+ end
178
243
  rescue => e
179
244
  logger.error e.full_message
180
- Parallel::QuietExceptionWrapper.new(name, ::Parallel::ExceptionWrapper.new(e))
245
+ Parallel::QuietExceptionWrapper.new(last_description, ::Parallel::ExceptionWrapper.new(e))
181
246
  end
182
247
  end.flatten
183
248
 
@@ -185,8 +250,9 @@ module Switchman
185
250
  unless errors.empty?
186
251
  raise errors.first.exception if errors.length == 1
187
252
 
253
+ errors_desc = errors.map(&:name).sort.join(", ")
188
254
  raise Errors::ParallelShardExecError,
189
- "The following database server(s) did not finish processing cleanly: #{errors.map(&:name).sort.join(', ')}",
255
+ "The following database server(s) did not finish processing cleanly: #{errors_desc}",
190
256
  cause: errors.first.exception
191
257
  end
192
258
 
@@ -195,14 +261,20 @@ module Switchman
195
261
 
196
262
  classes ||= []
197
263
 
198
- previous_shard = nil
199
264
  result = []
200
265
  ex = nil
266
+ old_stdout = $stdout
267
+ old_stderr = $stderr
201
268
  scope.each do |shard|
202
269
  # shard references a database server that isn't configured in this environment
203
270
  next unless shard.database_server
204
271
 
205
272
  shard.activate(*classes) do
273
+ if output
274
+ $stdout = Parallel::TransformingIO.new(output, $stdout)
275
+ $stderr = Parallel::TransformingIO.new(output, $stderr)
276
+ end
277
+
206
278
  result.concat Array.wrap(yield)
207
279
  rescue
208
280
  case exception
@@ -216,8 +288,10 @@ module Switchman
216
288
  else
217
289
  raise
218
290
  end
291
+ ensure
292
+ $stdout = old_stdout
293
+ $stderr = old_stderr
219
294
  end
220
- previous_shard = shard
221
295
  end
222
296
  raise ex if ex
223
297
 
@@ -241,7 +315,7 @@ module Switchman
241
315
  else
242
316
  shard = partition_object.shard
243
317
  end
244
- when Integer, /^\d+$/, /^(\d+)~(\d+)$/
318
+ when Integer, /\A\d+\Z/, /\A(\d+)~(\d+)\Z/
245
319
  local_id, shard = Shard.local_id_for(partition_object)
246
320
  local_id ||= partition_object
247
321
  object = local_id unless partition_proc
@@ -283,14 +357,14 @@ module Switchman
283
357
  case any_id
284
358
  when ::ActiveRecord::Base
285
359
  any_id.id
286
- when /^(\d+)~(-?\d+)$/
360
+ when /\A(\d+)~(-?\d+)\Z/
287
361
  local_id = $2.to_i
288
362
  signed_id_operation(local_id) do |id|
289
363
  return nil if id > IDS_PER_SHARD
290
364
 
291
365
  ($1.to_i * IDS_PER_SHARD) + id
292
366
  end
293
- when Integer, /^-?\d+$/
367
+ when Integer, /\A-?\d+\Z/
294
368
  any_id.to_i
295
369
  end
296
370
  end
@@ -374,7 +448,7 @@ module Switchman
374
448
  end
375
449
 
376
450
  def configure_connects_to
377
- full_connects_to_hash = DatabaseServer.all.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
451
+ full_connects_to_hash = DatabaseServer.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
378
452
  sharded_models.each do |klass|
379
453
  connects_to_hash = full_connects_to_hash.deep_dup
380
454
  if klass == UnshardedRecord
@@ -387,13 +461,16 @@ module Switchman
387
461
  connects_to_hash.each do |(db_name, role_hash)|
388
462
  role_hash.each_key do |role|
389
463
  role_hash.delete(role) if klass.connection_handler.retrieve_connection_pool(
390
- klass.connection_specification_name, role: role, shard: db_name
464
+ klass.connection_specification_name, role:, shard: db_name
391
465
  )
392
466
  end
393
467
  end
394
468
  end
395
469
 
470
+ # this resets the default shard on rails 7.1+, but we want to preserve it
471
+ shard_was = klass.default_shard
396
472
  klass.connects_to shards: connects_to_hash
473
+ klass.default_shard = shard_was
397
474
  end
398
475
  end
399
476
 
@@ -427,8 +504,50 @@ module Switchman
427
504
  shard = nil unless shard.database_server
428
505
  shard
429
506
  end
507
+
508
+ # Determines the name of the current process, including arguments, but stripping
509
+ # any shebang from the invoked script, and any additional path info from the
510
+ # executable.
511
+ #
512
+ # @return [String]
513
+ def sanitized_process_title
514
+ # get the effective process name from `ps`; this will include any changes
515
+ # from Process.setproctitle _or_ assigning to $0.
516
+ parent_process_name = `ps -ocommand= -p#{Process.pid}`.strip
517
+ # Effective process titles may be shorter than the actual
518
+ # command; truncate our ARGV[0] so that they are comparable
519
+ # for the next step
520
+ argv0 = if parent_process_name.length < Process.argv0.length
521
+ Process.argv0[0..parent_process_name.length]
522
+ else
523
+ Process.argv0
524
+ end
525
+
526
+ # when running via a shebang, the `ps` output will include the shebang
527
+ # (i.e. it will be "ruby bin/rails c"); attempt to strip it off.
528
+ # Note that argv0 in this case will _only_ be `bin/rails` (no shebang,
529
+ # no arguments). We want to preserve the arguments we got from `ps`
530
+ if (index = parent_process_name.index(argv0))
531
+ parent_process_name.slice!(0...index)
532
+ end
533
+
534
+ # remove directories from the main executable to make more room
535
+ # for additional info
536
+ argv = parent_process_name.shellsplit
537
+ argv[0] = File.basename(argv[0])
538
+ argv.shelljoin
539
+ end
540
+
541
+ # @return [Array<String>] the list of database servers that are in the
542
+ # config, but don't have any shards on them
543
+ def non_existent_database_servers
544
+ @non_existent_database_servers ||=
545
+ Shard.distinct.pluck(:database_server_id).compact - DatabaseServer.all.map(&:id)
546
+ end
430
547
  end
431
548
 
549
+ delegate :region, :in_region?, :in_current_region?, to: :database_server
550
+
432
551
  def name
433
552
  unless instance_variable_defined?(:@name)
434
553
  # protect against re-entrancy
@@ -457,7 +576,7 @@ module Switchman
457
576
  end
458
577
 
459
578
  def description
460
- [database_server.id, name].compact.join(':')
579
+ [database_server.id, name].compact.join(":")
461
580
  end
462
581
 
463
582
  # Shards are always on the default shard
@@ -469,9 +588,13 @@ module Switchman
469
588
  id
470
589
  end
471
590
 
472
- def activate(*classes, &block)
591
+ def original_id_value
592
+ id
593
+ end
594
+
595
+ def activate(*classes, &)
473
596
  shards = hashify_classes(classes)
474
- Shard.activate(shards, &block)
597
+ Shard.activate(shards, &)
475
598
  end
476
599
 
477
600
  # for use from console ONLY
@@ -491,48 +614,39 @@ module Switchman
491
614
  end
492
615
 
493
616
  def drop_database
494
- raise('Cannot drop the database of the default shard') if default?
617
+ raise "Cannot drop the database of the default shard" if default?
495
618
  return unless read_attribute(:name)
496
619
 
497
620
  begin
498
- adapter = database_server.config[:adapter]
499
- sharding_config = Switchman.config || {}
500
- drop_statement = sharding_config[adapter]&.[](:drop_statement)
501
- drop_statement ||= sharding_config[:drop_statement]
502
- if drop_statement
503
- drop_statement = Array(drop_statement).dup.
504
- map { |statement| statement.gsub('%{name}', name) }
621
+ activate do
622
+ self.class.drop_database(name)
505
623
  end
624
+ rescue ::ActiveRecord::StatementInvalid => e
625
+ logger.error "Drop failed: #{e}"
626
+ end
627
+ end
506
628
 
507
- case adapter
508
- when 'mysql', 'mysql2'
509
- activate do
510
- ::GuardRail.activate(:deploy) do
511
- drop_statement ||= "DROP DATABASE #{name}"
512
- Array(drop_statement).each do |stmt|
513
- ::ActiveRecord::Base.connection.execute(stmt)
514
- end
515
- end
516
- end
517
- when 'postgresql'
518
- activate do
519
- ::GuardRail.activate(:deploy) do
520
- # Shut up, Postgres!
521
- conn = ::ActiveRecord::Base.connection
522
- old_proc = conn.raw_connection.set_notice_processor {}
523
- begin
524
- drop_statement ||= "DROP SCHEMA #{name} CASCADE"
525
- Array(drop_statement).each do |stmt|
526
- ::ActiveRecord::Base.connection.execute(stmt)
527
- end
528
- ensure
529
- conn.raw_connection.set_notice_processor(&old_proc) if old_proc
530
- end
531
- end
629
+ #
630
+ # Drops a specific database/schema from the currently active connection
631
+ #
632
+ def self.drop_database(name)
633
+ sharding_config = Switchman.config || {}
634
+ drop_statement = sharding_config["postgresql"]&.[](:drop_statement)
635
+ drop_statement ||= sharding_config[:drop_statement]
636
+ drop_statement = Array(drop_statement).map { |statement| statement.gsub("%{name}", name) } if drop_statement
637
+
638
+ ::GuardRail.activate(:deploy) do
639
+ # Shut up, Postgres!
640
+ conn = ::ActiveRecord::Base.connection
641
+ old_proc = conn.raw_connection.set_notice_processor {}
642
+ begin
643
+ drop_statement ||= "DROP SCHEMA #{name} CASCADE"
644
+ Array(drop_statement).each do |stmt|
645
+ ::ActiveRecord::Base.connection.execute(stmt)
532
646
  end
647
+ ensure
648
+ conn.raw_connection.set_notice_processor(&old_proc) if old_proc
533
649
  end
534
- rescue
535
- logger.info "Drop failed: #{$!}"
536
650
  end
537
651
  end
538
652
 
@@ -551,7 +665,7 @@ module Switchman
551
665
  end
552
666
 
553
667
  def destroy
554
- raise('Cannot destroy the default shard') if default?
668
+ raise("Cannot destroy the default shard") if default?
555
669
 
556
670
  super
557
671
  end
@@ -560,8 +674,8 @@ module Switchman
560
674
 
561
675
  def clear_cache
562
676
  Shard.default.activate do
563
- Switchman.cache.delete(['shard', id].join('/'))
564
- Switchman.cache.delete('default_shard') if default?
677
+ Switchman.cache.delete(["shard", id].join("/"))
678
+ Switchman.cache.delete("default_shard") if default?
565
679
  end
566
680
  self.class.clear_cache
567
681
  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
 
@@ -16,10 +16,10 @@ module Switchman
16
16
  payload[:shard] = {
17
17
  database_server_id: shard.database_server.id,
18
18
  id: shard.id,
19
- env: ::Rails.version < '7.0' ? @shard_host.pool.connection_klass&.current_role : @shard_host.pool.connection_class&.current_role
19
+ env: @shard_host.pool.connection_class&.current_role
20
20
  }
21
21
  end
22
- super name, payload
22
+ super
23
23
  end
24
24
  end
25
25
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ class SharedSchemaCache
5
+ def self.get_schema_cache(connection)
6
+ @schema_cache ||= ::ActiveRecord::ConnectionAdapters::SchemaCache.new(connection)
7
+ @schema_cache.connection = connection
8
+ @schema_cache
9
+ end
10
+ end
11
+ 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.1'
4
+ VERSION = "4.1.0"
5
5
  end