switchman 3.0.5 → 4.0.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 (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 +6 -15
  8. data/lib/switchman/active_record/associations.rb +331 -0
  9. data/lib/switchman/active_record/attribute_methods.rb +182 -77
  10. data/lib/switchman/active_record/base.rb +249 -46
  11. data/lib/switchman/active_record/calculations.rb +98 -44
  12. data/lib/switchman/active_record/connection_handler.rb +18 -0
  13. data/lib/switchman/active_record/connection_pool.rb +27 -28
  14. data/lib/switchman/active_record/database_configurations.rb +44 -6
  15. data/lib/switchman/active_record/finder_methods.rb +46 -16
  16. data/lib/switchman/active_record/log_subscriber.rb +11 -5
  17. data/lib/switchman/active_record/migration.rb +52 -5
  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 +12 -11
  22. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  23. data/lib/switchman/active_record/query_cache.rb +49 -20
  24. data/lib/switchman/active_record/query_methods.rb +202 -136
  25. data/lib/switchman/active_record/reflection.rb +1 -1
  26. data/lib/switchman/active_record/relation.rb +40 -28
  27. data/lib/switchman/active_record/spawn_methods.rb +2 -2
  28. data/lib/switchman/active_record/statement_cache.rb +11 -7
  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 +53 -0
  32. data/lib/switchman/active_support/cache.rb +25 -4
  33. data/lib/switchman/arel.rb +45 -7
  34. data/lib/switchman/call_super.rb +2 -2
  35. data/lib/switchman/database_server.rb +123 -79
  36. data/lib/switchman/default_shard.rb +14 -5
  37. data/lib/switchman/engine.rb +79 -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 +226 -241
  46. data/lib/switchman/sharded_instrumenter.rb +3 -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 +2 -2
  50. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  51. data/lib/switchman/version.rb +1 -1
  52. data/lib/switchman.rb +44 -12
  53. data/lib/tasks/switchman.rake +101 -54
  54. metadata +50 -58
  55. data/lib/switchman/active_record/association.rb +0 -206
  56. 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
@@ -39,15 +75,17 @@ module Switchman
39
75
  # the first time we need a dummy dummy for re-entrancy to avoid looping on ourselves
40
76
  @default ||= default
41
77
 
42
- # Now find the actual record, if it exists; rescue the fake default if the table doesn't exist
78
+ # Now find the actual record, if it exists
43
79
  @default = begin
44
- find_cached('default_shard') { Shard.where(default: true).take } || default
80
+ find_cached("default_shard") { Shard.where(default: true).take } || default
45
81
  rescue
46
82
  default
47
83
  end
48
84
 
49
85
  # 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)
86
+ if @default.database_server.instance_variable_defined?(:@primary_shard)
87
+ @default.database_server.remove_instance_variable(:@primary_shard)
88
+ end
51
89
 
52
90
  # and finally, check for cached references to the default shard on the existing connection
53
91
  sharded_models.each do |klass|
@@ -59,50 +97,61 @@ module Switchman
59
97
 
60
98
  def current(klass = ::ActiveRecord::Base)
61
99
  klass ||= ::ActiveRecord::Base
62
- klass.connection_pool.shard
100
+ klass.current_switchman_shard
63
101
  end
64
102
 
65
103
  def activate(shards)
66
104
  activated_classes = activate!(shards)
67
105
  yield
68
106
  ensure
69
- activated_classes.each do |klass|
70
- klass.connection_pool.shard_stack.pop
107
+ activated_classes&.each do |klass|
71
108
  klass.connected_to_stack.pop
72
109
  end
73
110
  end
74
111
 
75
112
  def activate!(shards)
76
- activated_classes = []
113
+ activated_classes = nil
77
114
  shards.each do |klass, shard|
78
115
  next if klass == UnshardedRecord
79
116
 
80
117
  next unless klass.current_shard != shard.database_server.id.to_sym ||
81
- klass.connection_pool.shard != shard
118
+ klass.current_switchman_shard != shard
82
119
 
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
120
+ (activated_classes ||= []) << klass
121
+ klass.connected_to_stack << { shard: shard.database_server.id.to_sym,
122
+ klasses: [klass],
123
+ switchman_shard: shard }
86
124
  end
87
125
  activated_classes
88
126
  end
89
127
 
128
+ def active_shards
129
+ sharded_models.filter_map do |klass|
130
+ [klass, current(klass)]
131
+ end.to_h
132
+ end
133
+
90
134
  def lookup(id)
91
135
  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'
136
+ return current if id_i == current.id || id == "self"
137
+ return default if id_i == default.id || id.nil? || id == "default"
94
138
 
95
139
  id = id_i
96
140
  raise ArgumentError if id.zero?
97
141
 
98
142
  unless cached_shards.key?(id)
99
143
  cached_shards[id] = Shard.default.activate do
100
- find_cached(['shard', id]) { find_by(id: id) }
144
+ find_cached(["shard", id]) { find_by(id: id) }
101
145
  end
102
146
  end
103
147
  cached_shards[id]
104
148
  end
105
149
 
150
+ def preload_cache
151
+ cached_shards.reverse_merge!(active_shards.values.index_by(&:id))
152
+ cached_shards.reverse_merge!(all.index_by(&:id))
153
+ end
154
+
106
155
  def clear_cache
107
156
  cached_shards.clear
108
157
  end
@@ -111,14 +160,13 @@ module Switchman
111
160
  #
112
161
  # * +shards+ - an array or relation of Shards to iterate over
113
162
  # * +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
163
+ # parallel: - true/false to execute in parallel, or an integer of how many
164
+ # sub-processes. Note that parallel invocation currently uses
165
+ # forking.
119
166
  # exception: - :ignore, :raise, :defer (wait until the end and raise the first
120
167
  # error), or a proc
121
- def with_each_shard(*args, parallel: false, max_procs: nil, exception: :raise, &block)
168
+ # output: - :simple, :decorated (with database_server_id:shard_name), custom lambda transformer
169
+ def with_each_shard(*args, parallel: false, exception: :raise, output: :simple)
122
170
  raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
123
171
 
124
172
  return Array.wrap(yield) unless default.is_a?(Shard)
@@ -133,190 +181,98 @@ module Switchman
133
181
  scope, classes = args
134
182
  end
135
183
 
136
- parallel = 1 if parallel == true
184
+ output = if output == :decorated
185
+ ->(arg) { "#{Shard.current.description}: #{arg}" }
186
+ elsif output == :simple
187
+ nil
188
+ else
189
+ output
190
+ end
191
+ parallel = [Environment.cpu_count || 2, 2].min if parallel == true
137
192
  parallel = 0 if parallel == false || parallel.nil?
138
193
 
139
194
  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?
195
+ if ::ActiveRecord::Relation === scope && scope.order_values.empty?
196
+ scope = scope.order(::Arel.sql("database_server_id IS NOT NULL, database_server_id, id"))
197
+ end
141
198
 
142
- if parallel.positive?
143
- max_procs = determine_max_procs(max_procs, parallel)
199
+ if parallel > 1
144
200
  if ::ActiveRecord::Relation === scope
145
201
  # 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
202
+ database_servers = scope.reorder("database_server_id").select(:database_server_id).distinct
203
+ .filter_map(&:database_server).uniq
148
204
  # nothing to do
149
205
  return if database_servers.count.zero?
150
206
 
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
207
+ scopes = database_servers.to_h do |server|
208
+ [server, scope.merge(server.shards)]
209
+ end
174
210
  else
175
211
  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
185
-
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}"
205
- 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
212
  end
213
213
 
214
214
  # clear connections prior to forking (no more queries will be executed in the parent,
215
215
  # and we want them gone so that we don't accidentally use them post-fork doing something
216
216
  # 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
281
- end
282
- 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
217
+ if ::Rails.version < "7.1"
218
+ ::ActiveRecord::Base.clear_all_connections!(nil)
219
+ else
220
+ ::ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
289
221
  end
290
222
 
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
223
+ parent_process_name = sanitized_process_title
224
+ ret = ::Parallel.map(scopes, in_processes: (scopes.length > 1) ? parallel : 0) do |server, subscope|
225
+ name = server.id
226
+ last_description = name
227
+
228
+ begin
229
+ max_length = 128 - name.length - 3
230
+ short_parent_name = parent_process_name[0..max_length] if max_length >= 0
231
+ new_title = [short_parent_name, name].join(" ")
232
+ Process.setproctitle(new_title)
233
+ Switchman.config[:on_fork_proc]&.call
234
+ with_each_shard(subscope,
235
+ classes,
236
+ exception: exception,
237
+ output: output || :decorated) do
238
+ last_description = Shard.current.description
239
+ Parallel::ResultWrapper.new(yield)
240
+ end
241
+ rescue => e
242
+ logger.error e.full_message
243
+ Parallel::QuietExceptionWrapper.new(last_description, ::Parallel::ExceptionWrapper.new(e))
244
+ end
245
+ end.flatten
301
246
 
247
+ errors = ret.select { |val| val.is_a?(Parallel::QuietExceptionWrapper) }
302
248
  unless errors.empty?
303
- raise ParallelShardExecError,
304
- "The following subprocesses did not exit cleanly: #{errors.sort.join(', ')}"
249
+ raise errors.first.exception if errors.length == 1
250
+
251
+ errors_desc = errors.map(&:name).sort.join(", ")
252
+ raise Errors::ParallelShardExecError,
253
+ "The following database server(s) did not finish processing cleanly: #{errors_desc}",
254
+ cause: errors.first.exception
305
255
  end
306
256
 
307
- return
257
+ return ret.map(&:result)
308
258
  end
309
259
 
310
260
  classes ||= []
311
261
 
312
- previous_shard = nil
313
262
  result = []
314
263
  ex = nil
264
+ old_stdout = $stdout
265
+ old_stderr = $stderr
315
266
  scope.each do |shard|
316
267
  # shard references a database server that isn't configured in this environment
317
268
  next unless shard.database_server
318
269
 
319
270
  shard.activate(*classes) do
271
+ if output
272
+ $stdout = Parallel::TransformingIO.new(output, $stdout)
273
+ $stderr = Parallel::TransformingIO.new(output, $stderr)
274
+ end
275
+
320
276
  result.concat Array.wrap(yield)
321
277
  rescue
322
278
  case exception
@@ -330,8 +286,10 @@ module Switchman
330
286
  else
331
287
  raise
332
288
  end
289
+ ensure
290
+ $stdout = old_stdout
291
+ $stderr = old_stderr
333
292
  end
334
- previous_shard = shard
335
293
  end
336
294
  raise ex if ex
337
295
 
@@ -355,7 +313,7 @@ module Switchman
355
313
  else
356
314
  shard = partition_object.shard
357
315
  end
358
- when Integer, /^\d+$/, /^(\d+)~(\d+)$/
316
+ when Integer, /\A\d+\Z/, /\A(\d+)~(\d+)\Z/
359
317
  local_id, shard = Shard.local_id_for(partition_object)
360
318
  local_id ||= partition_object
361
319
  object = local_id unless partition_proc
@@ -397,14 +355,14 @@ module Switchman
397
355
  case any_id
398
356
  when ::ActiveRecord::Base
399
357
  any_id.id
400
- when /^(\d+)~(-?\d+)$/
358
+ when /\A(\d+)~(-?\d+)\Z/
401
359
  local_id = $2.to_i
402
360
  signed_id_operation(local_id) do |id|
403
361
  return nil if id > IDS_PER_SHARD
404
362
 
405
- $1.to_i * IDS_PER_SHARD + id
363
+ ($1.to_i * IDS_PER_SHARD) + id
406
364
  end
407
- when Integer, /^-?\d+$/
365
+ when Integer, /\A-?\d+\Z/
408
366
  any_id.to_i
409
367
  end
410
368
  end
@@ -480,32 +438,15 @@ module Switchman
480
438
  shard || source_shard || Shard.current
481
439
  end
482
440
 
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
441
  private
501
442
 
502
443
  def add_sharded_model(klass)
503
444
  @sharded_models = (sharded_models + [klass]).freeze
504
- initialize_sharding
445
+ configure_connects_to
505
446
  end
506
447
 
507
- def initialize_sharding
508
- full_connects_to_hash = DatabaseServer.all.map { |db| [db.id.to_sym, db.connects_to_hash] }.to_h
448
+ def configure_connects_to
449
+ full_connects_to_hash = DatabaseServer.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
509
450
  sharded_models.each do |klass|
510
451
  connects_to_hash = full_connects_to_hash.deep_dup
511
452
  if klass == UnshardedRecord
@@ -524,7 +465,10 @@ module Switchman
524
465
  end
525
466
  end
526
467
 
468
+ # this resets the default shard on rails 7.1+, but we want to preserve it
469
+ shard_was = klass.default_shard
527
470
  klass.connects_to shards: connects_to_hash
471
+ klass.default_shard = shard_was
528
472
  end
529
473
  end
530
474
 
@@ -558,8 +502,50 @@ module Switchman
558
502
  shard = nil unless shard.database_server
559
503
  shard
560
504
  end
505
+
506
+ # Determines the name of the current process, including arguments, but stripping
507
+ # any shebang from the invoked script, and any additional path info from the
508
+ # executable.
509
+ #
510
+ # @return [String]
511
+ def sanitized_process_title
512
+ # get the effective process name from `ps`; this will include any changes
513
+ # from Process.setproctitle _or_ assigning to $0.
514
+ parent_process_name = `ps -ocommand= -p#{Process.pid}`.strip
515
+ # Effective process titles may be shorter than the actual
516
+ # command; truncate our ARGV[0] so that they are comparable
517
+ # for the next step
518
+ argv0 = if parent_process_name.length < Process.argv0.length
519
+ Process.argv0[0..parent_process_name.length]
520
+ else
521
+ Process.argv0
522
+ end
523
+
524
+ # when running via a shebang, the `ps` output will include the shebang
525
+ # (i.e. it will be "ruby bin/rails c"); attempt to strip it off.
526
+ # Note that argv0 in this case will _only_ be `bin/rails` (no shebang,
527
+ # no arguments). We want to preserve the arguments we got from `ps`
528
+ if (index = parent_process_name.index(argv0))
529
+ parent_process_name.slice!(0...index)
530
+ end
531
+
532
+ # remove directories from the main executable to make more room
533
+ # for additional info
534
+ argv = parent_process_name.shellsplit
535
+ argv[0] = File.basename(argv[0])
536
+ argv.shelljoin
537
+ end
538
+
539
+ # @return [Array<String>] the list of database servers that are in the
540
+ # config, but don't have any shards on them
541
+ def non_existent_database_servers
542
+ @non_existent_database_servers ||=
543
+ Shard.distinct.pluck(:database_server_id).compact - DatabaseServer.all.map(&:id)
544
+ end
561
545
  end
562
546
 
547
+ delegate :region, :in_region?, :in_current_region?, to: :database_server
548
+
563
549
  def name
564
550
  unless instance_variable_defined?(:@name)
565
551
  # protect against re-entrancy
@@ -588,7 +574,7 @@ module Switchman
588
574
  end
589
575
 
590
576
  def description
591
- [database_server.id, name].compact.join(':')
577
+ [database_server.id, name].compact.join(":")
592
578
  end
593
579
 
594
580
  # Shards are always on the default shard
@@ -596,6 +582,14 @@ module Switchman
596
582
  Shard.default
597
583
  end
598
584
 
585
+ def original_id
586
+ id
587
+ end
588
+
589
+ def original_id_value
590
+ id
591
+ end
592
+
599
593
  def activate(*classes, &block)
600
594
  shards = hashify_classes(classes)
601
595
  Shard.activate(shards, &block)
@@ -618,48 +612,39 @@ module Switchman
618
612
  end
619
613
 
620
614
  def drop_database
621
- raise('Cannot drop the database of the default shard') if default?
615
+ raise "Cannot drop the database of the default shard" if default?
622
616
  return unless read_attribute(:name)
623
617
 
624
618
  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) }
619
+ activate do
620
+ self.class.drop_database(name)
632
621
  end
622
+ rescue ::ActiveRecord::StatementInvalid => e
623
+ logger.error "Drop failed: #{e}"
624
+ end
625
+ end
633
626
 
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
627
+ #
628
+ # Drops a specific database/schema from the currently active connection
629
+ #
630
+ def self.drop_database(name)
631
+ sharding_config = Switchman.config || {}
632
+ drop_statement = sharding_config["postgresql"]&.[](:drop_statement)
633
+ drop_statement ||= sharding_config[:drop_statement]
634
+ drop_statement = Array(drop_statement).map { |statement| statement.gsub("%{name}", name) } if drop_statement
635
+
636
+ ::GuardRail.activate(:deploy) do
637
+ # Shut up, Postgres!
638
+ conn = ::ActiveRecord::Base.connection
639
+ old_proc = conn.raw_connection.set_notice_processor {}
640
+ begin
641
+ drop_statement ||= "DROP SCHEMA #{name} CASCADE"
642
+ Array(drop_statement).each do |stmt|
643
+ ::ActiveRecord::Base.connection.execute(stmt)
659
644
  end
645
+ ensure
646
+ conn.raw_connection.set_notice_processor(&old_proc) if old_proc
660
647
  end
661
- rescue
662
- logger.info "Drop failed: #{$!}"
663
648
  end
664
649
  end
665
650
 
@@ -668,7 +653,7 @@ module Switchman
668
653
  return nil unless local_id
669
654
 
670
655
  self.class.signed_id_operation(local_id) do |abs_id|
671
- abs_id + id * IDS_PER_SHARD
656
+ abs_id + (id * IDS_PER_SHARD)
672
657
  end
673
658
  end
674
659
 
@@ -678,7 +663,7 @@ module Switchman
678
663
  end
679
664
 
680
665
  def destroy
681
- raise('Cannot destroy the default shard') if default?
666
+ raise("Cannot destroy the default shard") if default?
682
667
 
683
668
  super
684
669
  end
@@ -687,8 +672,8 @@ module Switchman
687
672
 
688
673
  def clear_cache
689
674
  Shard.default.activate do
690
- Switchman.cache.delete(['shard', id].join('/'))
691
- Switchman.cache.delete('default_shard') if default?
675
+ Switchman.cache.delete(["shard", id].join("/"))
676
+ Switchman.cache.delete("default_shard") if default?
692
677
  end
693
678
  self.class.clear_cache
694
679
  end