switchman 3.0.14 → 3.5.20

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +16 -15
  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/action_controller/caching.rb +2 -2
  6. data/lib/switchman/active_record/abstract_adapter.rb +6 -6
  7. data/lib/switchman/active_record/associations.rb +365 -0
  8. data/lib/switchman/active_record/attribute_methods.rb +188 -99
  9. data/lib/switchman/active_record/base.rb +185 -40
  10. data/lib/switchman/active_record/calculations.rb +64 -40
  11. data/lib/switchman/active_record/connection_handler.rb +18 -0
  12. data/lib/switchman/active_record/connection_pool.rb +24 -5
  13. data/lib/switchman/active_record/database_configurations.rb +37 -13
  14. data/lib/switchman/active_record/finder_methods.rb +46 -16
  15. data/lib/switchman/active_record/log_subscriber.rb +11 -5
  16. data/lib/switchman/active_record/migration.rb +52 -8
  17. data/lib/switchman/active_record/model_schema.rb +1 -1
  18. data/lib/switchman/active_record/persistence.rb +31 -3
  19. data/lib/switchman/active_record/postgresql_adapter.rb +11 -10
  20. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  21. data/lib/switchman/active_record/query_cache.rb +49 -20
  22. data/lib/switchman/active_record/query_methods.rb +187 -136
  23. data/lib/switchman/active_record/reflection.rb +1 -1
  24. data/lib/switchman/active_record/relation.rb +33 -26
  25. data/lib/switchman/active_record/spawn_methods.rb +2 -2
  26. data/lib/switchman/active_record/statement_cache.rb +11 -7
  27. data/lib/switchman/active_record/table_definition.rb +1 -1
  28. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  29. data/lib/switchman/active_record/test_fixtures.rb +26 -16
  30. data/lib/switchman/active_support/cache.rb +20 -1
  31. data/lib/switchman/arel.rb +34 -18
  32. data/lib/switchman/call_super.rb +8 -2
  33. data/lib/switchman/database_server.rb +91 -45
  34. data/lib/switchman/default_shard.rb +14 -5
  35. data/lib/switchman/engine.rb +79 -126
  36. data/lib/switchman/environment.rb +2 -2
  37. data/lib/switchman/errors.rb +17 -2
  38. data/lib/switchman/guard_rail/relation.rb +8 -10
  39. data/lib/switchman/guard_rail.rb +5 -0
  40. data/lib/switchman/parallel.rb +68 -0
  41. data/lib/switchman/r_spec_helper.rb +14 -11
  42. data/lib/switchman/rails.rb +2 -5
  43. data/{app/models → lib}/switchman/shard.rb +186 -189
  44. data/lib/switchman/sharded_instrumenter.rb +5 -1
  45. data/lib/switchman/shared_schema_cache.rb +11 -0
  46. data/lib/switchman/standard_error.rb +6 -5
  47. data/lib/switchman/test_helper.rb +2 -2
  48. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  49. data/lib/switchman/version.rb +1 -1
  50. data/lib/switchman.rb +44 -12
  51. data/lib/tasks/switchman.rake +74 -53
  52. metadata +42 -53
  53. data/lib/switchman/active_record/association.rb +0 -206
  54. data/lib/switchman/open4.rb +0 -80
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'switchman/database_server'
4
- require 'switchman/default_shard'
5
- require 'switchman/environment'
6
- require 'switchman/errors'
7
-
8
3
  module Switchman
9
4
  class Shard < UnshardedRecord
10
5
  # ten trillion possible ids per shard. yup.
@@ -18,6 +13,47 @@ module Switchman
18
13
 
19
14
  scope :primary, -> { where(name: nil).order(:database_server_id, :id).distinct_on(:database_server_id) }
20
15
 
16
+ scope :in_region, (lambda do |region, include_regionless: true|
17
+ next in_current_region if region.nil?
18
+
19
+ dbs_by_region = DatabaseServer.group_by(&:region)
20
+ db_count_in_this_region = dbs_by_region[region]&.length.to_i
21
+ db_count_in_this_region += dbs_by_region[nil]&.length.to_i if include_regionless
22
+ non_existent_database_servers = Shard.send(:non_existent_database_servers)
23
+ db_count_in_other_regions = DatabaseServer.all.length -
24
+ db_count_in_this_region +
25
+ non_existent_database_servers.length
26
+
27
+ dbs_in_this_region = dbs_by_region[region]&.map(&:id) || []
28
+ dbs_in_this_region += dbs_by_region[nil]&.map(&:id) || [] if include_regionless
29
+
30
+ if db_count_in_this_region <= db_count_in_other_regions
31
+ if dbs_in_this_region.include?(Shard.default.database_server.id)
32
+ where("database_server_id IN (?) OR database_server_id IS NULL", dbs_in_this_region)
33
+ else
34
+ where(database_server_id: dbs_in_this_region)
35
+ end
36
+ elsif db_count_in_other_regions.zero?
37
+ all
38
+ else
39
+ dbs_not_in_this_region = DatabaseServer.map(&:id) - dbs_in_this_region + non_existent_database_servers
40
+ if dbs_in_this_region.include?(Shard.default.database_server.id)
41
+ where("database_server_id NOT IN (?) OR database_server_id IS NULL", dbs_not_in_this_region)
42
+ else
43
+ where.not(database_server_id: dbs_not_in_this_region)
44
+ end
45
+ end
46
+ end)
47
+
48
+ scope :in_current_region, (lambda do |include_regionless: true|
49
+ # sharding isn't set up? maybe we're in tests, or a somehow degraded environment
50
+ # either way there's only one shard, and we always want to see it
51
+ return [default] unless default.is_a?(Switchman::Shard)
52
+ return all if !Switchman.region || DatabaseServer.none?(&:region)
53
+
54
+ in_region(Switchman.region, include_regionless: include_regionless)
55
+ end)
56
+
21
57
  class << self
22
58
  def sharded_models
23
59
  @sharded_models ||= [::ActiveRecord::Base, UnshardedRecord].freeze
@@ -41,16 +77,15 @@ module Switchman
41
77
 
42
78
  # Now find the actual record, if it exists
43
79
  @default = begin
44
- find_cached('default_shard') { Shard.where(default: true).take } || default
45
- # If we are *super* early in boot, the connection pool won't exist; we don't want to fill in the default shard yet
46
- # Otherwise, rescue the fake default if the table doesn't exist
80
+ find_cached("default_shard") { Shard.where(default: true).take } || default
47
81
  rescue
48
- sharding_initialized ? default : nil
82
+ default
49
83
  end
50
- return default unless @default
51
84
 
52
85
  # make sure this is not erroneously cached
53
- @default.database_server.remove_instance_variable(:@primary_shard) if @default.database_server.instance_variable_defined?(:@primary_shard)
86
+ if @default.database_server.instance_variable_defined?(:@primary_shard)
87
+ @default.database_server.remove_instance_variable(:@primary_shard)
88
+ end
54
89
 
55
90
  # and finally, check for cached references to the default shard on the existing connection
56
91
  sharded_models.each do |klass|
@@ -83,28 +118,30 @@ module Switchman
83
118
  klass.current_switchman_shard != shard
84
119
 
85
120
  (activated_classes ||= []) << klass
86
- klass.connected_to_stack << { shard: shard.database_server.id.to_sym, klasses: [klass], switchman_shard: shard }
121
+ klass.connected_to_stack << { shard: shard.database_server.id.to_sym,
122
+ klasses: [klass],
123
+ switchman_shard: shard }
87
124
  end
88
125
  activated_classes
89
126
  end
90
127
 
91
128
  def active_shards
92
- sharded_models.map do |klass|
129
+ sharded_models.filter_map do |klass|
93
130
  [klass, current(klass)]
94
- end.compact.to_h
131
+ end.to_h
95
132
  end
96
133
 
97
134
  def lookup(id)
98
135
  id_i = id.to_i
99
- return current if id_i == current.id || id == 'self'
100
- return default if id_i == default.id || id.nil? || id == 'default'
136
+ return current if id_i == current.id || id == "self"
137
+ return default if id_i == default.id || id.nil? || id == "default"
101
138
 
102
139
  id = id_i
103
140
  raise ArgumentError if id.zero?
104
141
 
105
142
  unless cached_shards.key?(id)
106
143
  cached_shards[id] = Shard.default.activate do
107
- find_cached(['shard', id]) { find_by(id: id) }
144
+ find_cached(["shard", id]) { find_by(id: id) }
108
145
  end
109
146
  end
110
147
  cached_shards[id]
@@ -125,11 +162,11 @@ module Switchman
125
162
  # * +classes+ - an array of classes to activate
126
163
  # parallel: - true/false to execute in parallel, or an integer of how many
127
164
  # sub-processes. Note that parallel invocation currently uses
128
- # forking, so should be used sparingly because you cannot get
129
- # results back
165
+ # forking.
130
166
  # exception: - :ignore, :raise, :defer (wait until the end and raise the first
131
167
  # error), or a proc
132
- def with_each_shard(*args, parallel: false, exception: :raise, &block)
168
+ # output: - :simple, :decorated (with database_server_id:shard_name)
169
+ def with_each_shard(*args, parallel: false, exception: :raise, output: :simple)
133
170
  raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
134
171
 
135
172
  return Array.wrap(yield) unless default.is_a?(Shard)
@@ -148,160 +185,84 @@ module Switchman
148
185
  parallel = 0 if parallel == false || parallel.nil?
149
186
 
150
187
  scope ||= Shard.all
151
- scope = scope.order(::Arel.sql('database_server_id IS NOT NULL, database_server_id, id')) if ::ActiveRecord::Relation === scope && scope.order_values.empty?
188
+ if ::ActiveRecord::Relation === scope && scope.order_values.empty?
189
+ scope = scope.order(::Arel.sql("database_server_id IS NOT NULL, database_server_id, id"))
190
+ end
152
191
 
153
192
  if parallel > 1
154
193
  if ::ActiveRecord::Relation === scope
155
194
  # still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
156
- database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
157
- map(&:database_server).compact.uniq
195
+ database_servers = scope.reorder("database_server_id").select(:database_server_id).distinct
196
+ .filter_map(&:database_server).uniq
158
197
  # nothing to do
159
198
  return if database_servers.count.zero?
160
199
 
161
200
  scopes = database_servers.to_h do |server|
162
- [server, server.shards.merge(scope)]
201
+ [server, scope.merge(server.shards)]
163
202
  end
164
203
  else
165
204
  scopes = scope.group_by(&:database_server)
166
205
  end
167
206
 
168
- exception_pipes = []
169
- pids = []
170
- out_fds = []
171
- err_fds = []
172
- pid_to_name_map = {}
173
- fd_to_name_map = {}
174
- errors = []
175
-
176
- wait_for_output = lambda do
177
- ready, = IO.select(out_fds + err_fds)
178
- ready.each do |fd|
179
- if fd.eof?
180
- fd.close
181
- out_fds.delete(fd)
182
- err_fds.delete(fd)
183
- next
184
- end
185
- line = fd.readline
186
- puts "#{fd_to_name_map[fd]}: #{line}"
187
- end
188
- end
189
-
190
- # only one process; don't bother forking
191
- return with_each_shard(scopes.first.last, classes, exception: exception, &block) if scopes.length == 1
192
-
193
207
  # clear connections prior to forking (no more queries will be executed in the parent,
194
208
  # and we want them gone so that we don't accidentally use them post-fork doing something
195
209
  # silly like dealloc'ing prepared statements)
196
- ::ActiveRecord::Base.clear_all_connections!
210
+ if ::Rails.version < "7.1"
211
+ ::ActiveRecord::Base.clear_all_connections!(nil)
212
+ else
213
+ ::ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
214
+ end
197
215
 
198
- scopes.each do |server, subscope|
216
+ parent_process_name = sanitized_process_title
217
+ ret = ::Parallel.map(scopes, in_processes: (scopes.length > 1) ? parallel : 0) do |server, subscope|
199
218
  name = server.id
219
+ last_description = name
200
220
 
201
- exception_pipe = IO.pipe
202
- exception_pipes << exception_pipe
203
- pid, io_in, io_out, io_err = Open4.pfork4(lambda do
204
- Switchman.config[:on_fork_proc]&.call
205
-
206
- # set a pretty name for the process title, up to 128 characters
207
- # (we don't actually know the limit, depending on how the process
208
- # was started)
209
- # first, simplify the binary name by stripping directories,
210
- # then truncate arguments as necessary
211
- bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
212
- max_length = 128 - bin.length - name.length - 3
213
- args = ARGV.join(' ')
214
- args = args[0..max_length] if max_length >= 0
215
- new_title = [bin, args, name].join(' ')
221
+ begin
222
+ max_length = 128 - name.length - 3
223
+ short_parent_name = parent_process_name[0..max_length] if max_length >= 0
224
+ new_title = [short_parent_name, name].join(" ")
216
225
  Process.setproctitle(new_title)
217
-
218
- with_each_shard(subscope, classes, exception: exception, &block)
219
- exception_pipe.last.close
220
- rescue Exception => e # rubocop:disable Lint/RescueException
221
- begin
222
- dumped = Marshal.dump(e)
223
- dumped = nil if dumped.length > 64 * 1024
224
- rescue
225
- dumped = nil
226
- end
227
-
228
- if dumped.nil?
229
- # couldn't dump the exception; create a copy with just
230
- # the message and the backtrace
231
- e2 = e.class.new(e.message)
232
- backtrace = e.backtrace
233
- # truncate excessively long backtraces
234
- backtrace = backtrace[0...25] + ['...'] + backtrace[-25..] if backtrace.length > 50
235
- e2.set_backtrace(backtrace)
236
- e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
237
- dumped = Marshal.dump(e2)
238
- end
239
- exception_pipe.last.set_encoding(dumped.encoding)
240
- exception_pipe.last.write(dumped)
241
- exception_pipe.last.flush
242
- exception_pipe.last.close
243
- exit! 1
244
- end)
245
- exception_pipe.last.close
246
- pids << pid
247
- io_in.close # don't care about writing to stdin
248
- out_fds << io_out
249
- err_fds << io_err
250
- pid_to_name_map[pid] = name
251
- fd_to_name_map[io_out] = name
252
- fd_to_name_map[io_err] = name
253
-
254
- while pids.count >= parallel
255
- while out_fds.count >= parallel
256
- # wait for output if we've hit the parallel limit
257
- wait_for_output.call
226
+ Switchman.config[:on_fork_proc]&.call
227
+ with_each_shard(subscope, classes, exception: exception, output: :decorated) do
228
+ last_description = Shard.current.description
229
+ Parallel::ResultWrapper.new(yield)
258
230
  end
259
- # we've gotten all the output from one fd so wait for its child process to exit
260
- found_pid, status = Process.wait2
261
- pids.delete(found_pid)
262
- errors << pid_to_name_map[found_pid] if status.exitstatus != 0
231
+ rescue => e
232
+ logger.error e.full_message
233
+ Parallel::QuietExceptionWrapper.new(last_description, ::Parallel::ExceptionWrapper.new(e))
263
234
  end
264
- # we've gotten all the output from one fd so wait for its child process to exit
265
- found_pid, status = Process.wait2
266
- pids.delete(found_pid)
267
- errors << pid_to_name_map[found_pid] if status.exitstatus != 0
268
- end
269
-
270
- wait_for_output.call while out_fds.any? || err_fds.any?
271
- pids.each do |pid|
272
- _, status = Process.waitpid2(pid)
273
- errors << pid_to_name_map[pid] if status.exitstatus != 0
274
- end
275
-
276
- # check for an exception; we only re-raise the first one
277
- exception_pipes.each do |exception_pipe|
278
- serialized_exception = exception_pipe.first.read
279
- next if serialized_exception.empty?
280
-
281
- ex = Marshal.load(serialized_exception) # rubocop:disable Security/MarshalLoad
282
- raise ex
283
- ensure
284
- exception_pipe.first.close
285
- end
235
+ end.flatten
286
236
 
237
+ errors = ret.select { |val| val.is_a?(Parallel::QuietExceptionWrapper) }
287
238
  unless errors.empty?
288
- raise ParallelShardExecError,
289
- "The following subprocesses did not exit cleanly: #{errors.sort.join(', ')}"
239
+ raise errors.first.exception if errors.length == 1
240
+
241
+ errors_desc = errors.map(&:name).sort.join(", ")
242
+ raise Errors::ParallelShardExecError,
243
+ "The following database server(s) did not finish processing cleanly: #{errors_desc}",
244
+ cause: errors.first.exception
290
245
  end
291
246
 
292
- return
247
+ return ret.map(&:result)
293
248
  end
294
249
 
295
250
  classes ||= []
296
251
 
297
- previous_shard = nil
298
252
  result = []
299
253
  ex = nil
254
+ old_stdout = $stdout
255
+ old_stderr = $stderr
300
256
  scope.each do |shard|
301
257
  # shard references a database server that isn't configured in this environment
302
258
  next unless shard.database_server
303
259
 
304
260
  shard.activate(*classes) do
261
+ if output == :decorated
262
+ $stdout = Parallel::PrefixingIO.new(shard.description, $stdout)
263
+ $stderr = Parallel::PrefixingIO.new(shard.description, $stderr)
264
+ end
265
+
305
266
  result.concat Array.wrap(yield)
306
267
  rescue
307
268
  case exception
@@ -315,8 +276,10 @@ module Switchman
315
276
  else
316
277
  raise
317
278
  end
279
+ ensure
280
+ $stdout = old_stdout
281
+ $stderr = old_stderr
318
282
  end
319
- previous_shard = shard
320
283
  end
321
284
  raise ex if ex
322
285
 
@@ -340,7 +303,7 @@ module Switchman
340
303
  else
341
304
  shard = partition_object.shard
342
305
  end
343
- when Integer, /^\d+$/, /^(\d+)~(\d+)$/
306
+ when Integer, /\A\d+\Z/, /\A(\d+)~(\d+)\Z/
344
307
  local_id, shard = Shard.local_id_for(partition_object)
345
308
  local_id ||= partition_object
346
309
  object = local_id unless partition_proc
@@ -382,14 +345,14 @@ module Switchman
382
345
  case any_id
383
346
  when ::ActiveRecord::Base
384
347
  any_id.id
385
- when /^(\d+)~(-?\d+)$/
348
+ when /\A(\d+)~(-?\d+)\Z/
386
349
  local_id = $2.to_i
387
350
  signed_id_operation(local_id) do |id|
388
351
  return nil if id > IDS_PER_SHARD
389
352
 
390
353
  ($1.to_i * IDS_PER_SHARD) + id
391
354
  end
392
- when Integer, /^-?\d+$/
355
+ when Integer, /\A-?\d+\Z/
393
356
  any_id.to_i
394
357
  end
395
358
  end
@@ -467,17 +430,13 @@ module Switchman
467
430
 
468
431
  private
469
432
 
470
- def sharding_initialized
471
- @sharding_initialized ||= false
472
- end
473
-
474
433
  def add_sharded_model(klass)
475
434
  @sharded_models = (sharded_models + [klass]).freeze
476
- initialize_sharding
435
+ configure_connects_to
477
436
  end
478
437
 
479
- def initialize_sharding
480
- full_connects_to_hash = DatabaseServer.all.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
438
+ def configure_connects_to
439
+ full_connects_to_hash = DatabaseServer.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
481
440
  sharded_models.each do |klass|
482
441
  connects_to_hash = full_connects_to_hash.deep_dup
483
442
  if klass == UnshardedRecord
@@ -496,10 +455,11 @@ module Switchman
496
455
  end
497
456
  end
498
457
 
458
+ # this resets the default shard on rails 7.1+, but we want to preserve it
459
+ shard_was = klass.default_shard
499
460
  klass.connects_to shards: connects_to_hash
461
+ klass.default_shard = shard_was
500
462
  end
501
-
502
- @sharding_initialized = true
503
463
  end
504
464
 
505
465
  # in-process caching
@@ -532,8 +492,50 @@ module Switchman
532
492
  shard = nil unless shard.database_server
533
493
  shard
534
494
  end
495
+
496
+ # Determines the name of the current process, including arguments, but stripping
497
+ # any shebang from the invoked script, and any additional path info from the
498
+ # executable.
499
+ #
500
+ # @return [String]
501
+ def sanitized_process_title
502
+ # get the effective process name from `ps`; this will include any changes
503
+ # from Process.setproctitle _or_ assigning to $0.
504
+ parent_process_name = `ps -ocommand= -p#{Process.pid}`.strip
505
+ # Effective process titles may be shorter than the actual
506
+ # command; truncate our ARGV[0] so that they are comparable
507
+ # for the next step
508
+ argv0 = if parent_process_name.length < Process.argv0.length
509
+ Process.argv0[0..parent_process_name.length]
510
+ else
511
+ Process.argv0
512
+ end
513
+
514
+ # when running via a shebang, the `ps` output will include the shebang
515
+ # (i.e. it will be "ruby bin/rails c"); attempt to strip it off.
516
+ # Note that argv0 in this case will _only_ be `bin/rails` (no shebang,
517
+ # no arguments). We want to preserve the arguments we got from `ps`
518
+ if (index = parent_process_name.index(argv0))
519
+ parent_process_name.slice!(0...index)
520
+ end
521
+
522
+ # remove directories from the main executable to make more room
523
+ # for additional info
524
+ argv = parent_process_name.shellsplit
525
+ argv[0] = File.basename(argv[0])
526
+ argv.shelljoin
527
+ end
528
+
529
+ # @return [Array<String>] the list of database servers that are in the
530
+ # config, but don't have any shards on them
531
+ def non_existent_database_servers
532
+ @non_existent_database_servers ||=
533
+ Shard.distinct.pluck(:database_server_id).compact - DatabaseServer.all.map(&:id)
534
+ end
535
535
  end
536
536
 
537
+ delegate :region, :in_region?, :in_current_region?, to: :database_server
538
+
537
539
  def name
538
540
  unless instance_variable_defined?(:@name)
539
541
  # protect against re-entrancy
@@ -562,7 +564,7 @@ module Switchman
562
564
  end
563
565
 
564
566
  def description
565
- [database_server.id, name].compact.join(':')
567
+ [database_server.id, name].compact.join(":")
566
568
  end
567
569
 
568
570
  # Shards are always on the default shard
@@ -574,6 +576,10 @@ module Switchman
574
576
  id
575
577
  end
576
578
 
579
+ def original_id_value
580
+ id
581
+ end
582
+
577
583
  def activate(*classes, &block)
578
584
  shards = hashify_classes(classes)
579
585
  Shard.activate(shards, &block)
@@ -596,48 +602,39 @@ module Switchman
596
602
  end
597
603
 
598
604
  def drop_database
599
- raise('Cannot drop the database of the default shard') if default?
605
+ raise "Cannot drop the database of the default shard" if default?
600
606
  return unless read_attribute(:name)
601
607
 
602
608
  begin
603
- adapter = database_server.config[:adapter]
604
- sharding_config = Switchman.config || {}
605
- drop_statement = sharding_config[adapter]&.[](:drop_statement)
606
- drop_statement ||= sharding_config[:drop_statement]
607
- if drop_statement
608
- drop_statement = Array(drop_statement).dup.
609
- map { |statement| statement.gsub('%{name}', name) }
609
+ activate do
610
+ self.class.drop_database(name)
610
611
  end
612
+ rescue ::ActiveRecord::StatementInvalid => e
613
+ logger.error "Drop failed: #{e}"
614
+ end
615
+ end
611
616
 
612
- case adapter
613
- when 'mysql', 'mysql2'
614
- activate do
615
- ::GuardRail.activate(:deploy) do
616
- drop_statement ||= "DROP DATABASE #{name}"
617
- Array(drop_statement).each do |stmt|
618
- ::ActiveRecord::Base.connection.execute(stmt)
619
- end
620
- end
621
- end
622
- when 'postgresql'
623
- activate do
624
- ::GuardRail.activate(:deploy) do
625
- # Shut up, Postgres!
626
- conn = ::ActiveRecord::Base.connection
627
- old_proc = conn.raw_connection.set_notice_processor {}
628
- begin
629
- drop_statement ||= "DROP SCHEMA #{name} CASCADE"
630
- Array(drop_statement).each do |stmt|
631
- ::ActiveRecord::Base.connection.execute(stmt)
632
- end
633
- ensure
634
- conn.raw_connection.set_notice_processor(&old_proc) if old_proc
635
- end
636
- end
617
+ #
618
+ # Drops a specific database/schema from the currently active connection
619
+ #
620
+ def self.drop_database(name)
621
+ sharding_config = Switchman.config || {}
622
+ drop_statement = sharding_config["postgresql"]&.[](:drop_statement)
623
+ drop_statement ||= sharding_config[:drop_statement]
624
+ drop_statement = Array(drop_statement).map { |statement| statement.gsub("%{name}", name) } if drop_statement
625
+
626
+ ::GuardRail.activate(:deploy) do
627
+ # Shut up, Postgres!
628
+ conn = ::ActiveRecord::Base.connection
629
+ old_proc = conn.raw_connection.set_notice_processor {}
630
+ begin
631
+ drop_statement ||= "DROP SCHEMA #{name} CASCADE"
632
+ Array(drop_statement).each do |stmt|
633
+ ::ActiveRecord::Base.connection.execute(stmt)
637
634
  end
635
+ ensure
636
+ conn.raw_connection.set_notice_processor(&old_proc) if old_proc
638
637
  end
639
- rescue
640
- logger.info "Drop failed: #{$!}"
641
638
  end
642
639
  end
643
640
 
@@ -656,7 +653,7 @@ module Switchman
656
653
  end
657
654
 
658
655
  def destroy
659
- raise('Cannot destroy the default shard') if default?
656
+ raise("Cannot destroy the default shard") if default?
660
657
 
661
658
  super
662
659
  end
@@ -665,8 +662,8 @@ module Switchman
665
662
 
666
663
  def clear_cache
667
664
  Shard.default.activate do
668
- Switchman.cache.delete(['shard', id].join('/'))
669
- Switchman.cache.delete('default_shard') if default?
665
+ Switchman.cache.delete(["shard", id].join("/"))
666
+ Switchman.cache.delete("default_shard") if default?
670
667
  end
671
668
  self.class.clear_cache
672
669
  end
@@ -16,7 +16,11 @@ module Switchman
16
16
  payload[:shard] = {
17
17
  database_server_id: shard.database_server.id,
18
18
  id: shard.id,
19
- env: shard.database_server.guard_rail_environment
19
+ env: if ::Rails.version < "7.0"
20
+ @shard_host.pool.connection_klass&.current_role
21
+ else
22
+ @shard_host.pool.connection_class&.current_role
23
+ end
20
24
  }
21
25
  end
22
26
  super name, payload
@@ -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
@@ -6,16 +6,17 @@ module Switchman
6
6
  super
7
7
  # These seem to get themselves into a bad state if we try to lookup shards while processing
8
8
  return if is_a?(IO::EAGAINWaitReadable)
9
+
9
10
  return if Thread.current[:switchman_error_handler]
10
11
 
11
12
  begin
12
13
  Thread.current[:switchman_error_handler] = true
13
14
 
14
- begin
15
- @active_shards = Shard.active_shards if defined?(Shard)
16
- rescue
17
- # If we hit an error really early in boot, activerecord may not be initialized yet
18
- end
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
19
20
  ensure
20
21
  Thread.current[:switchman_error_handler] = nil
21
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
@@ -65,7 +65,7 @@ module Switchman
65
65
  if server == Shard.default.database_server
66
66
  server.shards.where(name: 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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Switchman
4
4
  class UnshardedRecord < ::ActiveRecord::Base
5
- sharded_model
5
+ self.abstract_class = true
6
6
  end
7
7
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- VERSION = '3.0.14'
4
+ VERSION = "3.5.20"
5
5
  end