switchman 3.0.1 → 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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +16 -15
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
  4. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +1 -1
  5. data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
  6. data/lib/switchman/action_controller/caching.rb +2 -2
  7. data/lib/switchman/active_record/abstract_adapter.rb +11 -18
  8. data/lib/switchman/active_record/associations.rb +315 -0
  9. data/lib/switchman/active_record/attribute_methods.rb +191 -79
  10. data/lib/switchman/active_record/base.rb +204 -50
  11. data/lib/switchman/active_record/calculations.rb +93 -50
  12. data/lib/switchman/active_record/connection_handler.rb +18 -0
  13. data/lib/switchman/active_record/connection_pool.rb +47 -34
  14. data/lib/switchman/active_record/database_configurations.rb +32 -6
  15. data/lib/switchman/active_record/finder_methods.rb +22 -16
  16. data/lib/switchman/active_record/log_subscriber.rb +3 -6
  17. data/lib/switchman/active_record/migration.rb +42 -14
  18. data/lib/switchman/active_record/model_schema.rb +1 -1
  19. data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
  20. data/lib/switchman/active_record/persistence.rb +37 -2
  21. data/lib/switchman/active_record/postgresql_adapter.rb +39 -20
  22. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  23. data/lib/switchman/active_record/query_cache.rb +26 -17
  24. data/lib/switchman/active_record/query_methods.rb +252 -135
  25. data/lib/switchman/active_record/reflection.rb +10 -3
  26. data/lib/switchman/active_record/relation.rb +154 -32
  27. data/lib/switchman/active_record/spawn_methods.rb +3 -7
  28. data/lib/switchman/active_record/statement_cache.rb +13 -9
  29. data/lib/switchman/active_record/table_definition.rb +1 -1
  30. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  31. data/lib/switchman/active_record/test_fixtures.rb +89 -0
  32. data/lib/switchman/active_support/cache.rb +25 -4
  33. data/lib/switchman/arel.rb +20 -7
  34. data/lib/switchman/call_super.rb +2 -2
  35. data/lib/switchman/database_server.rb +123 -83
  36. data/lib/switchman/default_shard.rb +14 -5
  37. data/lib/switchman/engine.rb +85 -131
  38. data/lib/switchman/environment.rb +2 -2
  39. data/lib/switchman/errors.rb +17 -2
  40. data/lib/switchman/guard_rail/relation.rb +7 -10
  41. data/lib/switchman/guard_rail.rb +5 -0
  42. data/lib/switchman/parallel.rb +68 -0
  43. data/lib/switchman/r_spec_helper.rb +17 -28
  44. data/lib/switchman/rails.rb +1 -4
  45. data/{app/models → lib}/switchman/shard.rb +229 -246
  46. data/lib/switchman/sharded_instrumenter.rb +9 -3
  47. data/lib/switchman/shared_schema_cache.rb +11 -0
  48. data/lib/switchman/standard_error.rb +15 -12
  49. data/lib/switchman/test_helper.rb +3 -3
  50. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  51. data/lib/switchman/version.rb +1 -1
  52. data/lib/switchman.rb +46 -12
  53. data/lib/tasks/switchman.rake +101 -54
  54. metadata +34 -176
  55. data/lib/switchman/active_record/association.rb +0 -206
  56. data/lib/switchman/open4.rb +0 -80
@@ -1,23 +1,61 @@
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.
11
6
  IDS_PER_SHARD = 10_000_000_000_000
12
7
 
8
+ # rubocop:disable Style/SymbolProc -- transforming to a lambda produces "no receiver given"
13
9
  # only allow one default
14
10
  validates_uniqueness_of :default, if: ->(s) { s.default? }
11
+ # rubocop:enable Style/SymbolProc
15
12
 
16
13
  after_save :clear_cache
17
14
  after_destroy :clear_cache
18
15
 
19
16
  scope :primary, -> { where(name: nil).order(:database_server_id, :id).distinct_on(:database_server_id) }
20
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
+
21
59
  class << self
22
60
  def sharded_models
23
61
  @sharded_models ||= [::ActiveRecord::Base, UnshardedRecord].freeze
@@ -39,15 +77,17 @@ module Switchman
39
77
  # the first time we need a dummy dummy for re-entrancy to avoid looping on ourselves
40
78
  @default ||= default
41
79
 
42
- # Now find the actual record, if it exists; rescue the fake default if the table doesn't exist
80
+ # Now find the actual record, if it exists
43
81
  @default = begin
44
- find_cached('default_shard') { Shard.where(default: true).take } || default
82
+ find_cached("default_shard") { Shard.where(default: true).take } || default
45
83
  rescue
46
84
  default
47
85
  end
48
86
 
49
87
  # make sure this is not erroneously cached
50
- @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
51
91
 
52
92
  # and finally, check for cached references to the default shard on the existing connection
53
93
  sharded_models.each do |klass|
@@ -59,50 +99,61 @@ module Switchman
59
99
 
60
100
  def current(klass = ::ActiveRecord::Base)
61
101
  klass ||= ::ActiveRecord::Base
62
- klass.connection_pool.shard
102
+ klass.current_switchman_shard
63
103
  end
64
104
 
65
105
  def activate(shards)
66
106
  activated_classes = activate!(shards)
67
107
  yield
68
108
  ensure
69
- activated_classes.each do |klass|
70
- klass.connection_pool.shard_stack.pop
109
+ activated_classes&.each do |klass|
71
110
  klass.connected_to_stack.pop
72
111
  end
73
112
  end
74
113
 
75
114
  def activate!(shards)
76
- activated_classes = []
115
+ activated_classes = nil
77
116
  shards.each do |klass, shard|
78
117
  next if klass == UnshardedRecord
79
118
 
80
119
  next unless klass.current_shard != shard.database_server.id.to_sym ||
81
- klass.connection_pool.shard != shard
120
+ klass.current_switchman_shard != shard
82
121
 
83
- activated_classes << klass
84
- klass.connected_to_stack << { shard: shard.database_server.id.to_sym, klasses: [klass] }
85
- klass.connection_pool.shard_stack << shard
122
+ (activated_classes ||= []) << klass
123
+ klass.connected_to_stack << { shard: shard.database_server.id.to_sym,
124
+ klasses: [klass],
125
+ switchman_shard: shard }
86
126
  end
87
127
  activated_classes
88
128
  end
89
129
 
130
+ def active_shards
131
+ sharded_models.filter_map do |klass|
132
+ [klass, current(klass)]
133
+ end.to_h
134
+ end
135
+
90
136
  def lookup(id)
91
137
  id_i = id.to_i
92
- return current if id_i == current.id || id == 'self'
93
- 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"
94
140
 
95
141
  id = id_i
96
142
  raise ArgumentError if id.zero?
97
143
 
98
144
  unless cached_shards.key?(id)
99
145
  cached_shards[id] = Shard.default.activate do
100
- find_cached(['shard', id]) { find_by(id: id) }
146
+ find_cached(["shard", id]) { find_by(id:) }
101
147
  end
102
148
  end
103
149
  cached_shards[id]
104
150
  end
105
151
 
152
+ def preload_cache
153
+ cached_shards.reverse_merge!(active_shards.values.index_by(&:id))
154
+ cached_shards.reverse_merge!(all.index_by(&:id))
155
+ end
156
+
106
157
  def clear_cache
107
158
  cached_shards.clear
108
159
  end
@@ -111,14 +162,13 @@ module Switchman
111
162
  #
112
163
  # * +shards+ - an array or relation of Shards to iterate over
113
164
  # * +classes+ - an array of classes to activate
114
- # parallel: - true/false to execute in parallel, or a integer of how many
115
- # sub-processes per database server. Note that parallel
116
- # invocation currently uses forking, so should be used sparingly
117
- # because errors are not raised, and you cannot get results back
118
- # max_procs: - only run this many parallel processes at a time
165
+ # parallel: - true/false to execute in parallel, or an integer of how many
166
+ # sub-processes. Note that parallel invocation currently uses
167
+ # forking.
119
168
  # exception: - :ignore, :raise, :defer (wait until the end and raise the first
120
169
  # error), or a proc
121
- def with_each_shard(*args, parallel: false, max_procs: nil, 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)
122
172
  raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
123
173
 
124
174
  return Array.wrap(yield) unless default.is_a?(Shard)
@@ -133,190 +183,94 @@ module Switchman
133
183
  scope, classes = args
134
184
  end
135
185
 
136
- parallel = 1 if parallel == true
186
+ output = if output == :decorated
187
+ ->(arg) { "#{Shard.current.description}: #{arg}" }
188
+ elsif output == :simple
189
+ nil
190
+ else
191
+ output
192
+ end
193
+ parallel = [Environment.cpu_count || 2, 2].min if parallel == true
137
194
  parallel = 0 if parallel == false || parallel.nil?
138
195
 
139
196
  scope ||= Shard.all
140
- 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
141
200
 
142
- if parallel.positive?
143
- max_procs = determine_max_procs(max_procs, parallel)
201
+ if parallel > 1
144
202
  if ::ActiveRecord::Relation === scope
145
203
  # still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
146
- database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
147
- map(&:database_server).compact.uniq
204
+ database_servers = scope.reorder("database_server_id").select(:database_server_id).distinct
205
+ .filter_map(&:database_server).uniq
148
206
  # nothing to do
149
- return if database_servers.count.zero?
150
-
151
- parallel = [(max_procs.to_f / database_servers.count).ceil, parallel].min if max_procs
152
-
153
- scopes = database_servers.map do |server|
154
- server_scope = server.shards.merge(scope)
155
- if parallel == 1
156
- subscopes = [server_scope]
157
- else
158
- subscopes = []
159
- total = server_scope.count
160
- ranges = []
161
- server_scope.find_ids_in_ranges(batch_size: (total.to_f / parallel).ceil) do |min, max|
162
- ranges << [min, max]
163
- end
164
- # create a half-open range on the last one
165
- ranges.last[1] = nil
166
- ranges.each do |min, max|
167
- subscope = server_scope.where('id>=?', min)
168
- subscope = subscope.where('id<=?', max) if max
169
- subscopes << subscope
170
- end
171
- end
172
- [server, subscopes]
173
- end.to_h
174
- else
175
- scopes = scope.group_by(&:database_server)
176
- if parallel > 1
177
- parallel = [(max_procs.to_f / scopes.count).ceil, parallel].min if max_procs
178
- scopes = scopes.map do |(server, shards)|
179
- [server, shards.in_groups(parallel, false).compact]
180
- end.to_h
181
- else
182
- scopes = scopes.map { |(server, shards)| [server, [shards]] }.to_h
183
- end
184
- end
207
+ return if database_servers.none?
185
208
 
186
- exception_pipes = []
187
- pids = []
188
- out_fds = []
189
- err_fds = []
190
- pid_to_name_map = {}
191
- fd_to_name_map = {}
192
- errors = []
193
-
194
- wait_for_output = lambda do
195
- ready, = IO.select(out_fds + err_fds)
196
- ready.each do |fd|
197
- if fd.eof?
198
- fd.close
199
- out_fds.delete(fd)
200
- err_fds.delete(fd)
201
- next
202
- end
203
- line = fd.readline
204
- puts "#{fd_to_name_map[fd]}: #{line}"
209
+ scopes = database_servers.to_h do |server|
210
+ [server, scope.merge(server.shards)]
205
211
  end
206
- end
207
-
208
- # only one process; don't bother forking
209
- if scopes.length == 1 && parallel == 1
210
- return with_each_shard(scopes.first.last.first, classes, exception: exception,
211
- &block)
212
+ else
213
+ scopes = scope.group_by(&:database_server)
212
214
  end
213
215
 
214
216
  # clear connections prior to forking (no more queries will be executed in the parent,
215
217
  # and we want them gone so that we don't accidentally use them post-fork doing something
216
218
  # silly like dealloc'ing prepared statements)
217
- ::ActiveRecord::Base.clear_all_connections!
218
-
219
- scopes.each do |server, subscopes|
220
- subscopes.each_with_index do |subscope, idx|
221
- name = if subscopes.length > 1
222
- "#{server.id} #{idx + 1}"
223
- else
224
- server.id
225
- end
226
-
227
- exception_pipe = IO.pipe
228
- exception_pipes << exception_pipe
229
- pid, io_in, io_out, io_err = Open4.pfork4(lambda do
230
- Switchman.config[:on_fork_proc]&.call
231
-
232
- # set a pretty name for the process title, up to 128 characters
233
- # (we don't actually know the limit, depending on how the process
234
- # was started)
235
- # first, simplify the binary name by stripping directories,
236
- # then truncate arguments as necessary
237
- bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
238
- max_length = 128 - bin.length - name.length - 3
239
- args = ARGV.join(' ')
240
- args = args[0..max_length] if max_length >= 0
241
- new_title = [bin, args, name].join(' ')
242
- Process.setproctitle(new_title)
243
-
244
- with_each_shard(subscope, classes, exception: exception, &block)
245
- exception_pipe.last.close
246
- rescue => e
247
- begin
248
- dumped = Marshal.dump(e)
249
- rescue
250
- # couldn't dump the exception; create a copy with just
251
- # the message and the backtrace
252
- e2 = e.class.new(e.message)
253
- e2.set_backtrace(e.backtrace)
254
- e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
255
- dumped = Marshal.dump(e2)
256
- end
257
- exception_pipe.last.set_encoding(dumped.encoding)
258
- exception_pipe.last.write(dumped)
259
- exception_pipe.last.flush
260
- exception_pipe.last.close
261
- exit! 1
262
- end)
263
- exception_pipe.last.close
264
- pids << pid
265
- io_in.close # don't care about writing to stdin
266
- out_fds << io_out
267
- err_fds << io_err
268
- pid_to_name_map[pid] = name
269
- fd_to_name_map[io_out] = name
270
- fd_to_name_map[io_err] = name
271
-
272
- while max_procs && pids.count >= max_procs
273
- while max_procs && out_fds.count >= max_procs
274
- # wait for output if we've hit the max_procs limit
275
- wait_for_output.call
276
- end
277
- # we've gotten all the output from one fd so wait for its child process to exit
278
- found_pid, status = Process.wait2
279
- pids.delete(found_pid)
280
- errors << pid_to_name_map[found_pid] if status.exitstatus != 0
219
+ ::ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
220
+
221
+ parent_process_name = sanitized_process_title
222
+ ret = ::Parallel.map(scopes, in_processes: (scopes.length > 1) ? parallel : 0) do |server, subscope|
223
+ name = server.id
224
+ last_description = name
225
+
226
+ begin
227
+ max_length = 128 - name.length - 3
228
+ short_parent_name = parent_process_name[0..max_length] if max_length >= 0
229
+ new_title = [short_parent_name, name].join(" ")
230
+ Process.setproctitle(new_title)
231
+ Switchman.config[:on_fork_proc]&.call
232
+ with_each_shard(subscope,
233
+ classes,
234
+ exception:,
235
+ output: output || :decorated) do
236
+ last_description = Shard.current.description
237
+ Parallel::ResultWrapper.new(yield)
281
238
  end
239
+ rescue => e
240
+ logger.error e.full_message
241
+ Parallel::QuietExceptionWrapper.new(last_description, ::Parallel::ExceptionWrapper.new(e))
282
242
  end
283
- end
284
-
285
- wait_for_output.call while out_fds.any? || err_fds.any?
286
- pids.each do |pid|
287
- _, status = Process.waitpid2(pid)
288
- errors << pid_to_name_map[pid] if status.exitstatus != 0
289
- end
290
-
291
- # check for an exception; we only re-raise the first one
292
- exception_pipes.each do |exception_pipe|
293
- serialized_exception = exception_pipe.first.read
294
- next if serialized_exception.empty?
295
-
296
- ex = Marshal.load(serialized_exception) # rubocop:disable Security/MarshalLoad
297
- raise ex
298
- ensure
299
- exception_pipe.first.close
300
- end
243
+ end.flatten
301
244
 
245
+ errors = ret.select { |val| val.is_a?(Parallel::QuietExceptionWrapper) }
302
246
  unless errors.empty?
303
- raise ParallelShardExecError,
304
- "The following subprocesses did not exit cleanly: #{errors.sort.join(', ')}"
247
+ raise errors.first.exception if errors.length == 1
248
+
249
+ errors_desc = errors.map(&:name).sort.join(", ")
250
+ raise Errors::ParallelShardExecError,
251
+ "The following database server(s) did not finish processing cleanly: #{errors_desc}",
252
+ cause: errors.first.exception
305
253
  end
306
254
 
307
- return
255
+ return ret.map(&:result)
308
256
  end
309
257
 
310
258
  classes ||= []
311
259
 
312
- previous_shard = nil
313
260
  result = []
314
261
  ex = nil
262
+ old_stdout = $stdout
263
+ old_stderr = $stderr
315
264
  scope.each do |shard|
316
265
  # shard references a database server that isn't configured in this environment
317
266
  next unless shard.database_server
318
267
 
319
268
  shard.activate(*classes) do
269
+ if output
270
+ $stdout = Parallel::TransformingIO.new(output, $stdout)
271
+ $stderr = Parallel::TransformingIO.new(output, $stderr)
272
+ end
273
+
320
274
  result.concat Array.wrap(yield)
321
275
  rescue
322
276
  case exception
@@ -330,8 +284,10 @@ module Switchman
330
284
  else
331
285
  raise
332
286
  end
287
+ ensure
288
+ $stdout = old_stdout
289
+ $stderr = old_stderr
333
290
  end
334
- previous_shard = shard
335
291
  end
336
292
  raise ex if ex
337
293
 
@@ -355,7 +311,7 @@ module Switchman
355
311
  else
356
312
  shard = partition_object.shard
357
313
  end
358
- when Integer, /^\d+$/, /^(\d+)~(\d+)$/
314
+ when Integer, /\A\d+\Z/, /\A(\d+)~(\d+)\Z/
359
315
  local_id, shard = Shard.local_id_for(partition_object)
360
316
  local_id ||= partition_object
361
317
  object = local_id unless partition_proc
@@ -397,14 +353,14 @@ module Switchman
397
353
  case any_id
398
354
  when ::ActiveRecord::Base
399
355
  any_id.id
400
- when /^(\d+)~(-?\d+)$/
356
+ when /\A(\d+)~(-?\d+)\Z/
401
357
  local_id = $2.to_i
402
358
  signed_id_operation(local_id) do |id|
403
359
  return nil if id > IDS_PER_SHARD
404
360
 
405
- $1.to_i * IDS_PER_SHARD + id
361
+ ($1.to_i * IDS_PER_SHARD) + id
406
362
  end
407
- when Integer, /^-?\d+$/
363
+ when Integer, /\A-?\d+\Z/
408
364
  any_id.to_i
409
365
  end
410
366
  end
@@ -480,32 +436,15 @@ module Switchman
480
436
  shard || source_shard || Shard.current
481
437
  end
482
438
 
483
- # given the provided option, determines whether we need to (and whether
484
- # it's possible) to determine a reasonable default.
485
- def determine_max_procs(max_procs_input, parallel_input = 2)
486
- max_procs = nil
487
- if max_procs_input
488
- max_procs = max_procs_input.to_i
489
- max_procs = nil if max_procs.zero?
490
- else
491
- return 1 if parallel_input.nil? || parallel_input < 1
492
-
493
- cpus = Environment.cpu_count
494
- max_procs = cpus * parallel_input if cpus&.positive?
495
- end
496
-
497
- max_procs
498
- end
499
-
500
439
  private
501
440
 
502
441
  def add_sharded_model(klass)
503
442
  @sharded_models = (sharded_models + [klass]).freeze
504
- initialize_sharding
443
+ configure_connects_to
505
444
  end
506
445
 
507
- def initialize_sharding
508
- full_connects_to_hash = DatabaseServer.all.map { |db| [db.id.to_sym, db.connects_to_hash] }.to_h
446
+ def configure_connects_to
447
+ full_connects_to_hash = DatabaseServer.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
509
448
  sharded_models.each do |klass|
510
449
  connects_to_hash = full_connects_to_hash.deep_dup
511
450
  if klass == UnshardedRecord
@@ -518,13 +457,16 @@ module Switchman
518
457
  connects_to_hash.each do |(db_name, role_hash)|
519
458
  role_hash.each_key do |role|
520
459
  role_hash.delete(role) if klass.connection_handler.retrieve_connection_pool(
521
- klass.connection_specification_name, role: role, shard: db_name
460
+ klass.connection_specification_name, role:, shard: db_name
522
461
  )
523
462
  end
524
463
  end
525
464
  end
526
465
 
466
+ # this resets the default shard on rails 7.1+, but we want to preserve it
467
+ shard_was = klass.default_shard
527
468
  klass.connects_to shards: connects_to_hash
469
+ klass.default_shard = shard_was
528
470
  end
529
471
  end
530
472
 
@@ -558,8 +500,50 @@ module Switchman
558
500
  shard = nil unless shard.database_server
559
501
  shard
560
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
561
543
  end
562
544
 
545
+ delegate :region, :in_region?, :in_current_region?, to: :database_server
546
+
563
547
  def name
564
548
  unless instance_variable_defined?(:@name)
565
549
  # protect against re-entrancy
@@ -588,7 +572,7 @@ module Switchman
588
572
  end
589
573
 
590
574
  def description
591
- [database_server.id, name].compact.join(':')
575
+ [database_server.id, name].compact.join(":")
592
576
  end
593
577
 
594
578
  # Shards are always on the default shard
@@ -596,9 +580,17 @@ module Switchman
596
580
  Shard.default
597
581
  end
598
582
 
599
- def activate(*classes, &block)
583
+ def original_id
584
+ id
585
+ end
586
+
587
+ def original_id_value
588
+ id
589
+ end
590
+
591
+ def activate(*classes, &)
600
592
  shards = hashify_classes(classes)
601
- Shard.activate(shards, &block)
593
+ Shard.activate(shards, &)
602
594
  end
603
595
 
604
596
  # for use from console ONLY
@@ -618,48 +610,39 @@ module Switchman
618
610
  end
619
611
 
620
612
  def drop_database
621
- raise('Cannot drop the database of the default shard') if default?
613
+ raise "Cannot drop the database of the default shard" if default?
622
614
  return unless read_attribute(:name)
623
615
 
624
616
  begin
625
- adapter = database_server.config[:adapter]
626
- sharding_config = Switchman.config || {}
627
- drop_statement = sharding_config[adapter]&.[](:drop_statement)
628
- drop_statement ||= sharding_config[:drop_statement]
629
- if drop_statement
630
- drop_statement = Array(drop_statement).dup.
631
- map { |statement| statement.gsub('%{name}', name) }
617
+ activate do
618
+ self.class.drop_database(name)
632
619
  end
620
+ rescue ::ActiveRecord::StatementInvalid => e
621
+ logger.error "Drop failed: #{e}"
622
+ end
623
+ end
633
624
 
634
- case adapter
635
- when 'mysql', 'mysql2'
636
- activate do
637
- ::GuardRail.activate(:deploy) do
638
- drop_statement ||= "DROP DATABASE #{name}"
639
- Array(drop_statement).each do |stmt|
640
- ::ActiveRecord::Base.connection.execute(stmt)
641
- end
642
- end
643
- end
644
- when 'postgresql'
645
- activate do
646
- ::GuardRail.activate(:deploy) do
647
- # Shut up, Postgres!
648
- conn = ::ActiveRecord::Base.connection
649
- old_proc = conn.raw_connection.set_notice_processor {}
650
- begin
651
- drop_statement ||= "DROP SCHEMA #{name} CASCADE"
652
- Array(drop_statement).each do |stmt|
653
- ::ActiveRecord::Base.connection.execute(stmt)
654
- end
655
- ensure
656
- conn.raw_connection.set_notice_processor(&old_proc) if old_proc
657
- end
658
- 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)
659
642
  end
643
+ ensure
644
+ conn.raw_connection.set_notice_processor(&old_proc) if old_proc
660
645
  end
661
- rescue
662
- logger.info "Drop failed: #{$!}"
663
646
  end
664
647
  end
665
648
 
@@ -668,7 +651,7 @@ module Switchman
668
651
  return nil unless local_id
669
652
 
670
653
  self.class.signed_id_operation(local_id) do |abs_id|
671
- abs_id + id * IDS_PER_SHARD
654
+ abs_id + (id * IDS_PER_SHARD)
672
655
  end
673
656
  end
674
657
 
@@ -678,7 +661,7 @@ module Switchman
678
661
  end
679
662
 
680
663
  def destroy
681
- raise('Cannot destroy the default shard') if default?
664
+ raise("Cannot destroy the default shard") if default?
682
665
 
683
666
  super
684
667
  end
@@ -687,8 +670,8 @@ module Switchman
687
670
 
688
671
  def clear_cache
689
672
  Shard.default.activate do
690
- Switchman.cache.delete(['shard', id].join('/'))
691
- Switchman.cache.delete('default_shard') if default?
673
+ Switchman.cache.delete(["shard", id].join("/"))
674
+ Switchman.cache.delete("default_shard") if default?
692
675
  end
693
676
  self.class.clear_cache
694
677
  end