switchman 2.1.0 → 3.0.6

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +10 -2
  3. data/app/models/switchman/shard.rb +270 -343
  4. data/app/models/switchman/unsharded_record.rb +7 -0
  5. data/db/migrate/20130328212039_create_switchman_shards.rb +1 -1
  6. data/db/migrate/20130328224244_create_default_shard.rb +5 -5
  7. data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +1 -0
  8. data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
  9. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +8 -6
  10. data/db/migrate/20190114212900_add_unique_name_indexes.rb +5 -3
  11. data/lib/switchman/action_controller/caching.rb +2 -2
  12. data/lib/switchman/active_record/abstract_adapter.rb +0 -8
  13. data/lib/switchman/active_record/association.rb +78 -89
  14. data/lib/switchman/active_record/attribute_methods.rb +127 -52
  15. data/lib/switchman/active_record/base.rb +83 -67
  16. data/lib/switchman/active_record/calculations.rb +73 -66
  17. data/lib/switchman/active_record/connection_pool.rb +12 -59
  18. data/lib/switchman/active_record/database_configurations/database_config.rb +13 -0
  19. data/lib/switchman/active_record/database_configurations.rb +34 -0
  20. data/lib/switchman/active_record/finder_methods.rb +11 -16
  21. data/lib/switchman/active_record/log_subscriber.rb +4 -8
  22. data/lib/switchman/active_record/migration.rb +19 -45
  23. data/lib/switchman/active_record/model_schema.rb +1 -1
  24. data/lib/switchman/active_record/persistence.rb +11 -6
  25. data/lib/switchman/active_record/postgresql_adapter.rb +33 -161
  26. data/lib/switchman/active_record/predicate_builder.rb +1 -1
  27. data/lib/switchman/active_record/query_cache.rb +18 -19
  28. data/lib/switchman/active_record/query_methods.rb +178 -193
  29. data/lib/switchman/active_record/reflection.rb +7 -22
  30. data/lib/switchman/active_record/relation.rb +32 -29
  31. data/lib/switchman/active_record/spawn_methods.rb +27 -29
  32. data/lib/switchman/active_record/statement_cache.rb +18 -35
  33. data/lib/switchman/active_record/tasks/database_tasks.rb +16 -0
  34. data/lib/switchman/active_record/test_fixtures.rb +43 -0
  35. data/lib/switchman/active_support/cache.rb +3 -5
  36. data/lib/switchman/arel.rb +13 -8
  37. data/lib/switchman/database_server.rb +130 -154
  38. data/lib/switchman/default_shard.rb +52 -16
  39. data/lib/switchman/engine.rb +65 -58
  40. data/lib/switchman/environment.rb +4 -8
  41. data/lib/switchman/errors.rb +1 -0
  42. data/lib/switchman/guard_rail/relation.rb +5 -7
  43. data/lib/switchman/guard_rail.rb +6 -19
  44. data/lib/switchman/r_spec_helper.rb +29 -57
  45. data/lib/switchman/rails.rb +14 -12
  46. data/lib/switchman/sharded_instrumenter.rb +1 -1
  47. data/lib/switchman/standard_error.rb +15 -3
  48. data/lib/switchman/test_helper.rb +5 -3
  49. data/lib/switchman/version.rb +1 -1
  50. data/lib/switchman.rb +3 -3
  51. data/lib/tasks/switchman.rake +61 -72
  52. metadata +90 -48
  53. data/lib/switchman/active_record/batches.rb +0 -11
  54. data/lib/switchman/active_record/connection_handler.rb +0 -190
  55. data/lib/switchman/active_record/where_clause_factory.rb +0 -36
  56. data/lib/switchman/connection_pool_proxy.rb +0 -173
  57. data/lib/switchman/schema_cache.rb +0 -28
@@ -6,44 +6,24 @@ require 'switchman/environment'
6
6
  require 'switchman/errors'
7
7
 
8
8
  module Switchman
9
- class Shard < ::ActiveRecord::Base
9
+ class Shard < UnshardedRecord
10
10
  # ten trillion possible ids per shard. yup.
11
11
  IDS_PER_SHARD = 10_000_000_000_000
12
12
 
13
- CATEGORIES =
14
- {
15
- # special cased to mean all other models
16
- :primary => nil,
17
- # special cased to not allow activating a shard other than the default
18
- :unsharded => [Shard]
19
- }
20
- private_constant :CATEGORIES
21
- @connection_specification_name = 'unsharded'
22
-
23
- if defined?(::ProtectedAttributes)
24
- attr_accessible :default, :name, :database_server
25
- end
26
-
27
13
  # only allow one default
28
- validates_uniqueness_of :default, :if => lambda { |s| s.default? }
14
+ validates_uniqueness_of :default, if: ->(s) { s.default? }
29
15
 
30
16
  after_save :clear_cache
31
17
  after_destroy :clear_cache
32
18
 
33
- after_rollback :on_rollback
34
-
35
19
  scope :primary, -> { where(name: nil).order(:database_server_id, :id).distinct_on(:database_server_id) }
36
20
 
37
21
  class << self
38
- def categories
39
- CATEGORIES.keys
22
+ def sharded_models
23
+ @sharded_models ||= [::ActiveRecord::Base, UnshardedRecord].freeze
40
24
  end
41
25
 
42
- def default(reload_deprecated = false, reload: false, with_fallback: false)
43
- if reload_deprecated
44
- reload = reload_deprecated
45
- ::ActiveSupport::Deprecation.warn("positional reload parameter to Switchman::Shard.default is deprecated; use `reload: true`")
46
- end
26
+ def default(reload: false, with_fallback: false)
47
27
  if !@default || reload
48
28
  # Have to create a dummy object so that several key methods still work
49
29
  # (it's easier to do this in one place here, and just assume that sharding
@@ -54,74 +34,77 @@ module Switchman
54
34
 
55
35
  # if we already have a default shard in place, and the caller wants
56
36
  # to use it as a fallback, use that instead of the dummy instance
57
- if with_fallback && @default
58
- default = @default
59
- end
37
+ default = @default if with_fallback && @default
60
38
 
61
39
  # the first time we need a dummy dummy for re-entrancy to avoid looping on ourselves
62
40
  @default ||= default
63
41
 
64
- # Now find the actual record, if it exists; rescue the fake default if the table doesn't exist
42
+ # Now find the actual record, if it exists
65
43
  @default = begin
66
- find_cached("default_shard") { Shard.where(default: true).take } || default
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
+ rescue ::ActiveRecord::ConnectionNotEstablished
47
+ nil
48
+ # rescue the fake default if the table doesn't exist
67
49
  rescue
68
50
  default
69
51
  end
70
-
71
- # rebuild current shard activations - it might have "another" default shard serialized there
72
- active_shards.replace(active_shards.dup.map do |category, shard|
73
- shard = Shard.lookup((!shard || shard.default?) ? 'default' : shard.id)
74
- [category, shard]
75
- end.to_h)
76
-
77
- activate!(primary: @default) if active_shards.empty?
52
+ return default unless @default
78
53
 
79
54
  # make sure this is not erroneously cached
80
- if @default.database_server.instance_variable_defined?(:@primary_shard)
81
- @default.database_server.remove_instance_variable(:@primary_shard)
82
- end
55
+ @default.database_server.remove_instance_variable(:@primary_shard) if @default.database_server.instance_variable_defined?(:@primary_shard)
83
56
 
84
57
  # and finally, check for cached references to the default shard on the existing connection
85
- if ::ActiveRecord::Base.connected? && ::ActiveRecord::Base.connection.shard.default?
86
- ::ActiveRecord::Base.connection.shard = @default
58
+ sharded_models.each do |klass|
59
+ klass.connection.shard = @default if klass.connected? && klass.connection.shard.default?
87
60
  end
88
61
  end
89
62
  @default
90
63
  end
91
64
 
92
- def current(category = :primary)
93
- active_shards[category] || Shard.default
65
+ def current(klass = ::ActiveRecord::Base)
66
+ klass ||= ::ActiveRecord::Base
67
+ klass.current_switchman_shard
94
68
  end
95
69
 
96
70
  def activate(shards)
97
- old_shards = activate!(shards)
71
+ activated_classes = activate!(shards)
98
72
  yield
99
73
  ensure
100
- active_shards.merge!(old_shards) if old_shards
74
+ activated_classes&.each do |klass|
75
+ klass.connected_to_stack.pop
76
+ end
101
77
  end
102
78
 
103
79
  def activate!(shards)
104
- old_shards = nil
105
- currently_active_shards = active_shards
106
- shards.each do |category, shard|
107
- next if category == :unsharded
108
- unless currently_active_shards[category] == shard
109
- old_shards ||= {}
110
- old_shards[category] = currently_active_shards[category]
111
- currently_active_shards[category] = shard
112
- end
80
+ activated_classes = nil
81
+ shards.each do |klass, shard|
82
+ next if klass == UnshardedRecord
83
+
84
+ next unless klass.current_shard != shard.database_server.id.to_sym ||
85
+ klass.current_switchman_shard != shard
86
+
87
+ (activated_classes ||= []) << klass
88
+ klass.connected_to_stack << { shard: shard.database_server.id.to_sym, klasses: [klass], switchman_shard: shard }
113
89
  end
114
- old_shards
90
+ activated_classes
91
+ end
92
+
93
+ def active_shards
94
+ sharded_models.map do |klass|
95
+ [klass, current(klass)]
96
+ end.compact.to_h
115
97
  end
116
98
 
117
99
  def lookup(id)
118
100
  id_i = id.to_i
119
101
  return current if id_i == current.id || id == 'self'
120
102
  return default if id_i == default.id || id.nil? || id == 'default'
103
+
121
104
  id = id_i
122
- raise ArgumentError if id == 0
105
+ raise ArgumentError if id.zero?
123
106
 
124
- unless cached_shards.has_key?(id)
107
+ unless cached_shards.key?(id)
125
108
  cached_shards[id] = Shard.default.activate do
126
109
  find_cached(['shard', id]) { find_by(id: id) }
127
110
  end
@@ -129,6 +112,11 @@ module Switchman
129
112
  cached_shards[id]
130
113
  end
131
114
 
115
+ def preload_cache
116
+ cached_shards.reverse_merge!(active_shards.values.index_by(&:id))
117
+ cached_shards.reverse_merge!(all.index_by(&:id))
118
+ end
119
+
132
120
  def clear_cache
133
121
  cached_shards.clear
134
122
  end
@@ -136,89 +124,47 @@ module Switchman
136
124
  # ==== Parameters
137
125
  #
138
126
  # * +shards+ - an array or relation of Shards to iterate over
139
- # * +categories+ - an array of categories to activate
140
- # * +options+ -
141
- # :parallel - true/false to execute in parallel, or a integer of how many
142
- # sub-processes per database server. Note that parallel
143
- # invocation currently uses forking, so should be used sparingly
144
- # because errors are not raised, and you cannot get results back
145
- # :max_procs - only run this many parallel processes at a time
146
- # :exception - :ignore, :raise, :defer (wait until the end and raise the first
127
+ # * +classes+ - an array of classes to activate
128
+ # parallel: - true/false to execute in parallel, or an integer of how many
129
+ # sub-processes. Note that parallel invocation currently uses
130
+ # forking, so should be used sparingly because you cannot get
131
+ # results back
132
+ # exception: - :ignore, :raise, :defer (wait until the end and raise the first
147
133
  # error), or a proc
148
- def with_each_shard(*args)
149
- raise ArgumentError, "wrong number of arguments (#{args.length} for 0...3)" if args.length > 3
134
+ def with_each_shard(*args, parallel: false, exception: :raise, &block)
135
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
150
136
 
151
- unless default.is_a?(Shard)
152
- return Array.wrap(yield)
153
- end
137
+ return Array.wrap(yield) unless default.is_a?(Shard)
154
138
 
155
- options = args.extract_options!
156
139
  if args.length == 1
157
- if Array === args.first && args.first.first.is_a?(Symbol)
158
- categories = args.first
140
+ if Array === args.first && args.first.first.is_a?(Class)
141
+ classes = args.first
159
142
  else
160
143
  scope = args.first
161
144
  end
162
145
  else
163
- scope, categories = args
146
+ scope, classes = args
164
147
  end
165
148
 
166
- parallel = case options[:parallel]
167
- when true
168
- 1
169
- when false, nil
170
- 0
171
- else
172
- options[:parallel]
173
- end
174
- options.delete(:parallel)
149
+ parallel = [Environment.cpu_count || 2, 2].min if parallel == true
150
+ parallel = 0 if parallel == false || parallel.nil?
175
151
 
176
152
  scope ||= Shard.all
177
- if ::ActiveRecord::Relation === scope && scope.order_values.empty?
178
- scope = scope.order(::Arel.sql("database_server_id IS NOT NULL, database_server_id, id"))
179
- end
153
+ scope = scope.order(::Arel.sql('database_server_id IS NOT NULL, database_server_id, id')) if ::ActiveRecord::Relation === scope && scope.order_values.empty?
180
154
 
181
- if parallel > 0
182
- max_procs = determine_max_procs(options.delete(:max_procs), parallel)
155
+ if parallel > 1
183
156
  if ::ActiveRecord::Relation === scope
184
157
  # still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
185
158
  database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
186
- map(&:database_server).compact.uniq
159
+ map(&:database_server).compact.uniq
187
160
  # nothing to do
188
- return if database_servers.count == 0
189
- parallel = [(max_procs.to_f / database_servers.count).ceil, parallel].min if max_procs
190
-
191
- scopes = Hash[database_servers.map do |server|
192
- server_scope = server.shards.merge(scope)
193
- if parallel == 1
194
- subscopes = [server_scope]
195
- else
196
- subscopes = []
197
- total = server_scope.count
198
- ranges = []
199
- server_scope.find_ids_in_ranges(:batch_size => (total.to_f / parallel).ceil) do |min, max|
200
- ranges << [min, max]
201
- end
202
- # create a half-open range on the last one
203
- ranges.last[1] = nil
204
- ranges.each do |min, max|
205
- subscope = server_scope.where("id>=?", min)
206
- subscope = subscope.where("id<=?", max) if max
207
- subscopes << subscope
208
- end
209
- end
210
- [server, subscopes]
211
- end]
161
+ return if database_servers.count.zero?
162
+
163
+ scopes = database_servers.map do |server|
164
+ [server, server.shards.merge(scope)]
165
+ end.to_h
212
166
  else
213
167
  scopes = scope.group_by(&:database_server)
214
- if parallel > 1
215
- parallel = [(max_procs.to_f / scopes.count).ceil, parallel].min if max_procs
216
- scopes = Hash[scopes.map do |(server, shards)|
217
- [server, shards.in_groups(parallel, false).compact]
218
- end]
219
- else
220
- scopes = Hash[scopes.map { |(server, shards)| [server, [shards]] }]
221
- end
222
168
  end
223
169
 
224
170
  exception_pipes = []
@@ -229,8 +175,8 @@ module Switchman
229
175
  fd_to_name_map = {}
230
176
  errors = []
231
177
 
232
- wait_for_output = lambda do |out_fds, err_fds, fd_to_name_map|
233
- ready, _ = IO.select(out_fds + err_fds)
178
+ wait_for_output = lambda do
179
+ ready, = IO.select(out_fds + err_fds)
234
180
  ready.each do |fd|
235
181
  if fd.eof?
236
182
  fd.close
@@ -244,99 +190,86 @@ module Switchman
244
190
  end
245
191
 
246
192
  # only one process; don't bother forking
247
- if scopes.length == 1 && parallel == 1
248
- return with_each_shard(scopes.first.last.first, categories, options) { yield }
249
- end
193
+ return with_each_shard(scopes.first.last, classes, exception: exception, &block) if scopes.length == 1
250
194
 
251
195
  # clear connections prior to forking (no more queries will be executed in the parent,
252
196
  # and we want them gone so that we don't accidentally use them post-fork doing something
253
197
  # silly like dealloc'ing prepared statements)
254
198
  ::ActiveRecord::Base.clear_all_connections!
255
199
 
256
- scopes.each do |server, subscopes|
257
- subscopes.each_with_index do |subscope, idx|
258
- if subscopes.length > 1
259
- name = "#{server.id} #{idx + 1}"
260
- else
261
- name = server.id
200
+ scopes.each do |server, subscope|
201
+ name = server.id
202
+
203
+ exception_pipe = IO.pipe
204
+ exception_pipes << exception_pipe
205
+ pid, io_in, io_out, io_err = Open4.pfork4(lambda do
206
+ Switchman.config[:on_fork_proc]&.call
207
+
208
+ # set a pretty name for the process title, up to 128 characters
209
+ # (we don't actually know the limit, depending on how the process
210
+ # was started)
211
+ # first, simplify the binary name by stripping directories,
212
+ # then truncate arguments as necessary
213
+ bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
214
+ max_length = 128 - bin.length - name.length - 3
215
+ args = ARGV.join(' ')
216
+ args = args[0..max_length] if max_length >= 0
217
+ new_title = [bin, args, name].join(' ')
218
+ Process.setproctitle(new_title)
219
+
220
+ with_each_shard(subscope, classes, exception: exception, &block)
221
+ exception_pipe.last.close
222
+ rescue Exception => e # rubocop:disable Lint/RescueException
223
+ begin
224
+ dumped = Marshal.dump(e)
225
+ dumped = nil if dumped.length > 64 * 1024
226
+ rescue
227
+ dumped = nil
262
228
  end
263
229
 
264
- exception_pipe = IO.pipe
265
- exception_pipes << exception_pipe
266
- pid, io_in, io_out, io_err = Open4.pfork4(lambda do
267
- begin
268
- Switchman.config[:on_fork_proc]&.call
269
-
270
- # set a pretty name for the process title, up to 128 characters
271
- # (we don't actually know the limit, depending on how the process
272
- # was started)
273
- # first, simplify the binary name by stripping directories,
274
- # then truncate arguments as necessary
275
- bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
276
- max_length = 128 - bin.length - name.length - 3
277
- args = ARGV.join(" ")
278
- if max_length >= 0
279
- args = args[0..max_length]
280
- end
281
- new_title = [bin, args, name].join(" ")
282
- Process.setproctitle(new_title)
283
-
284
- with_each_shard(subscope, categories, options) { yield }
285
- exception_pipe.last.close
286
- rescue Exception => e
287
- begin
288
- dumped = Marshal.dump(e)
289
- dumped = nil if dumped.length > 64 * 1024
290
- rescue
291
- dumped = nil
292
- end
293
-
294
- if dumped.nil?
295
- # couldn't dump the exception; create a copy with just
296
- # the message and the backtrace
297
- e2 = e.class.new(e.message)
298
- backtrace = e.backtrace
299
- # truncate excessively long backtraces
300
- if backtrace.length > 50
301
- backtrace = backtrace[0...25] + ['...'] + backtrace[-25..-1]
302
- end
303
- e2.set_backtrace(backtrace)
304
- e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
305
- dumped = Marshal.dump(e2)
306
- end
307
-
308
- exception_pipe.last.set_encoding(dumped.encoding)
309
- exception_pipe.last.write(dumped)
310
- exception_pipe.last.flush
311
- exception_pipe.last.close
312
- exit! 1
313
- end
314
- end)
230
+ if dumped.nil?
231
+ # couldn't dump the exception; create a copy with just
232
+ # the message and the backtrace
233
+ e2 = e.class.new(e.message)
234
+ backtrace = e.backtrace
235
+ # truncate excessively long backtraces
236
+ backtrace = backtrace[0...25] + ['...'] + backtrace[-25..] if backtrace.length > 50
237
+ e2.set_backtrace(backtrace)
238
+ e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
239
+ dumped = Marshal.dump(e2)
240
+ end
241
+ exception_pipe.last.set_encoding(dumped.encoding)
242
+ exception_pipe.last.write(dumped)
243
+ exception_pipe.last.flush
315
244
  exception_pipe.last.close
316
- pids << pid
317
- io_in.close # don't care about writing to stdin
318
- out_fds << io_out
319
- err_fds << io_err
320
- pid_to_name_map[pid] = name
321
- fd_to_name_map[io_out] = name
322
- fd_to_name_map[io_err] = name
323
-
324
- while max_procs && pids.count >= max_procs
325
- while max_procs && out_fds.count >= max_procs
326
- # wait for output if we've hit the max_procs limit
327
- wait_for_output.call(out_fds, err_fds, fd_to_name_map)
328
- end
329
- # we've gotten all the output from one fd so wait for its child process to exit
330
- found_pid, status = Process.wait2
331
- pids.delete(found_pid)
332
- errors << pid_to_name_map[found_pid] if status.exitstatus != 0
245
+ exit! 1
246
+ end)
247
+ exception_pipe.last.close
248
+ pids << pid
249
+ io_in.close # don't care about writing to stdin
250
+ out_fds << io_out
251
+ err_fds << io_err
252
+ pid_to_name_map[pid] = name
253
+ fd_to_name_map[io_out] = name
254
+ fd_to_name_map[io_err] = name
255
+
256
+ while pids.count >= parallel
257
+ while out_fds.count >= parallel
258
+ # wait for output if we've hit the parallel limit
259
+ wait_for_output.call
333
260
  end
261
+ # we've gotten all the output from one fd so wait for its child process to exit
262
+ found_pid, status = Process.wait2
263
+ pids.delete(found_pid)
264
+ errors << pid_to_name_map[found_pid] if status.exitstatus != 0
334
265
  end
266
+ # we've gotten all the output from one fd so wait for its child process to exit
267
+ found_pid, status = Process.wait2
268
+ pids.delete(found_pid)
269
+ errors << pid_to_name_map[found_pid] if status.exitstatus != 0
335
270
  end
336
271
 
337
- while out_fds.any? || err_fds.any?
338
- wait_for_output.call(out_fds, err_fds, fd_to_name_map)
339
- end
272
+ wait_for_output.call while out_fds.any? || err_fds.any?
340
273
  pids.each do |pid|
341
274
  _, status = Process.waitpid2(pid)
342
275
  errors << pid_to_name_map[pid] if status.exitstatus != 0
@@ -344,67 +277,51 @@ module Switchman
344
277
 
345
278
  # check for an exception; we only re-raise the first one
346
279
  exception_pipes.each do |exception_pipe|
347
- begin
348
- serialized_exception = exception_pipe.first.read
349
- next if serialized_exception.empty?
350
- exception = Marshal.load(serialized_exception)
351
- raise exception
352
- ensure
353
- exception_pipe.first.close
354
- end
280
+ serialized_exception = exception_pipe.first.read
281
+ next if serialized_exception.empty?
282
+
283
+ ex = Marshal.load(serialized_exception) # rubocop:disable Security/MarshalLoad
284
+ raise ex
285
+ ensure
286
+ exception_pipe.first.close
355
287
  end
356
288
 
357
289
  unless errors.empty?
358
- raise ParallelShardExecError.new("The following subprocesses did not exit cleanly: #{errors.sort.join(", ")}")
290
+ raise ParallelShardExecError,
291
+ "The following subprocesses did not exit cleanly: #{errors.sort.join(', ')}"
359
292
  end
293
+
360
294
  return
361
295
  end
362
296
 
363
- categories ||= []
297
+ classes ||= []
364
298
 
365
299
  previous_shard = nil
366
- close_connections_if_needed = lambda do |shard|
367
- # prune the prior connection unless it happened to be the same
368
- if previous_shard && shard != previous_shard && !previous_shard.database_server.shareable?
369
- previous_shard.activate do
370
- ::GuardRail.activated_environments.each do |env|
371
- ::GuardRail.activate(env) do
372
- if ::ActiveRecord::Base.connected? && ::ActiveRecord::Base.connection.open_transactions == 0
373
- ::ActiveRecord::Base.connection_pool.current_pool.disconnect!
374
- end
375
- end
376
- end
377
- end
378
- end
379
- end
380
-
381
300
  result = []
382
- exception = nil
301
+ ex = nil
383
302
  scope.each do |shard|
384
303
  # shard references a database server that isn't configured in this environment
385
304
  next unless shard.database_server
386
- close_connections_if_needed.call(shard)
387
- shard.activate(*categories) do
388
- begin
389
- result.concat Array.wrap(yield)
390
- rescue
391
- case options[:exception]
392
- when :ignore
393
- when :defer
394
- exception ||= $!
395
- when Proc
396
- options[:exception].call
397
- when :raise
398
- raise
399
- else
400
- raise
401
- end
305
+
306
+ shard.activate(*classes) do
307
+ result.concat Array.wrap(yield)
308
+ rescue
309
+ case exception
310
+ when :ignore
311
+ # ignore
312
+ when :defer
313
+ ex ||= $!
314
+ when Proc
315
+ exception.call
316
+ # when :raise
317
+ else
318
+ raise
402
319
  end
403
320
  end
404
321
  previous_shard = shard
405
322
  end
406
- close_connections_if_needed.call(Shard.current)
407
- raise exception if exception
323
+ raise ex if ex
324
+
408
325
  result
409
326
  end
410
327
 
@@ -413,22 +330,22 @@ module Switchman
413
330
  array.each do |object|
414
331
  partition_object = partition_proc ? partition_proc.call(object) : object
415
332
  case partition_object
416
- when Shard
417
- shard = partition_object
418
- when ::ActiveRecord::Base
419
- if partition_object.respond_to?(:associated_shards)
420
- partition_object.associated_shards.each do |a_shard|
421
- shard_arrays[a_shard] ||= []
422
- shard_arrays[a_shard] << object
423
- end
424
- next
425
- else
426
- shard = partition_object.shard
333
+ when Shard
334
+ shard = partition_object
335
+ when ::ActiveRecord::Base
336
+ if partition_object.respond_to?(:associated_shards)
337
+ partition_object.associated_shards.each do |a_shard|
338
+ shard_arrays[a_shard] ||= []
339
+ shard_arrays[a_shard] << object
427
340
  end
428
- when Integer, /^\d+$/, /^(\d+)~(\d+)$/
429
- local_id, shard = Shard.local_id_for(partition_object)
430
- local_id ||= partition_object
431
- object = local_id if !partition_proc
341
+ next
342
+ else
343
+ shard = partition_object.shard
344
+ end
345
+ when Integer, /^\d+$/, /^(\d+)~(\d+)$/
346
+ local_id, shard = Shard.local_id_for(partition_object)
347
+ local_id ||= partition_object
348
+ object = local_id unless partition_proc
432
349
  end
433
350
  shard ||= Shard.current
434
351
  shard_arrays[shard] ||= []
@@ -437,7 +354,7 @@ module Switchman
437
354
  # TODO: use with_each_shard (or vice versa) to get
438
355
  # connection management and parallelism benefits
439
356
  shard_arrays.inject([]) do |results, (shard, objects)|
440
- results.concat shard.activate { Array.wrap(yield objects) }
357
+ results.concat(shard.activate { Array.wrap(yield objects) })
441
358
  end
442
359
  end
443
360
 
@@ -449,7 +366,7 @@ module Switchman
449
366
  # stay as provided. This assumes no consumer
450
367
  # will return a nil value from the block.
451
368
  def signed_id_operation(input_id)
452
- sign = input_id < 0 ? -1 : 1
369
+ sign = input_id.negative? ? -1 : 1
453
370
  output = yield input_id.abs
454
371
  output * sign
455
372
  end
@@ -457,9 +374,10 @@ module Switchman
457
374
  # converts an AR object, integral id, string id, or string short-global-id to a
458
375
  # integral id. nil if it can't be interpreted
459
376
  def integral_id_for(any_id)
460
- if any_id.is_a?(::Arel::Nodes::Casted)
461
- any_id = any_id.val
462
- elsif any_id.is_a?(::Arel::Nodes::BindParam) && ::Rails.version >= "5.2"
377
+ case any_id
378
+ when ::Arel::Nodes::Casted
379
+ any_id = any_id.value
380
+ when ::Arel::Nodes::BindParam
463
381
  any_id = any_id.value.value_before_type_cast
464
382
  end
465
383
 
@@ -470,12 +388,11 @@ module Switchman
470
388
  local_id = $2.to_i
471
389
  signed_id_operation(local_id) do |id|
472
390
  return nil if id > IDS_PER_SHARD
473
- $1.to_i * IDS_PER_SHARD + id
391
+
392
+ ($1.to_i * IDS_PER_SHARD) + id
474
393
  end
475
394
  when Integer, /^-?\d+$/
476
395
  any_id.to_i
477
- else
478
- nil
479
396
  end
480
397
  end
481
398
 
@@ -487,11 +404,12 @@ module Switchman
487
404
  def local_id_for(any_id)
488
405
  id = integral_id_for(any_id)
489
406
  return NIL_NIL_ID unless id
407
+
490
408
  return_shard = nil
491
409
  local_id = signed_id_operation(id) do |abs_id|
492
410
  if abs_id < IDS_PER_SHARD
493
411
  abs_id
494
- elsif return_shard = lookup(abs_id / IDS_PER_SHARD)
412
+ elsif (return_shard = lookup(abs_id / IDS_PER_SHARD))
495
413
  abs_id % IDS_PER_SHARD
496
414
  else
497
415
  return NIL_NIL_ID
@@ -508,8 +426,10 @@ module Switchman
508
426
  integral_id = integral_id_for(any_id)
509
427
  local_id, shard = local_id_for(integral_id)
510
428
  return integral_id unless local_id
429
+
511
430
  shard ||= source_shard
512
431
  return local_id if shard == target_shard
432
+
513
433
  shard.global_id_for(local_id)
514
434
  end
515
435
 
@@ -520,6 +440,7 @@ module Switchman
520
440
  local_id, shard = local_id_for(any_id)
521
441
  return any_id unless local_id
522
442
  return local_id unless shard
443
+
523
444
  "#{shard.id}~#{local_id}"
524
445
  end
525
446
 
@@ -528,6 +449,7 @@ module Switchman
528
449
  def global_id_for(any_id, source_shard = nil)
529
450
  id = integral_id_for(any_id)
530
451
  return any_id unless id
452
+
531
453
  signed_id_operation(id) do |abs_id|
532
454
  if abs_id >= IDS_PER_SHARD
533
455
  abs_id
@@ -540,29 +462,42 @@ module Switchman
540
462
 
541
463
  def shard_for(any_id, source_shard = nil)
542
464
  return any_id.shard if any_id.is_a?(::ActiveRecord::Base)
465
+
543
466
  _, shard = local_id_for(any_id)
544
467
  shard || source_shard || Shard.current
545
468
  end
546
469
 
547
- # given the provided option, determines whether we need to (and whether
548
- # it's possible) to determine a reasonable default.
549
- def determine_max_procs(max_procs_input, parallel_input=2)
550
- max_procs = nil
551
- if max_procs_input
552
- max_procs = max_procs_input.to_i
553
- max_procs = nil if max_procs == 0
554
- else
555
- return 1 if parallel_input.nil? || parallel_input < 1
556
- cpus = Environment.cpu_count
557
- if cpus && cpus > 0
558
- max_procs = cpus * parallel_input
470
+ private
471
+
472
+ def add_sharded_model(klass)
473
+ @sharded_models = (sharded_models + [klass]).freeze
474
+ initialize_sharding
475
+ end
476
+
477
+ def initialize_sharding
478
+ full_connects_to_hash = DatabaseServer.all.map { |db| [db.id.to_sym, db.connects_to_hash] }.to_h
479
+ sharded_models.each do |klass|
480
+ connects_to_hash = full_connects_to_hash.deep_dup
481
+ if klass == UnshardedRecord
482
+ # no need to mention other databases for the unsharded category
483
+ connects_to_hash = { ::Rails.env.to_sym => DatabaseServer.find(nil).connects_to_hash }
559
484
  end
560
- end
561
485
 
562
- return max_procs
486
+ # prune things we're already connected to
487
+ if klass.connection_specification_name == klass.name
488
+ connects_to_hash.each do |(db_name, role_hash)|
489
+ role_hash.each_key do |role|
490
+ role_hash.delete(role) if klass.connection_handler.retrieve_connection_pool(
491
+ klass.connection_specification_name, role: role, shard: db_name
492
+ )
493
+ end
494
+ end
495
+ end
496
+
497
+ klass.connects_to shards: connects_to_hash
498
+ end
563
499
  end
564
500
 
565
- private
566
501
  # in-process caching
567
502
  def cached_shards
568
503
  @cached_shards ||= {}.compare_by_identity
@@ -593,10 +528,6 @@ module Switchman
593
528
  shard = nil unless shard.database_server
594
529
  shard
595
530
  end
596
-
597
- def active_shards
598
- Thread.current[:active_shards] ||= {}.compare_by_identity
599
- end
600
531
  end
601
532
 
602
533
  def name
@@ -610,11 +541,11 @@ module Switchman
610
541
 
611
542
  def name=(name)
612
543
  write_attribute(:name, @name = name)
613
- remove_instance_variable(:@name) if name == nil
544
+ remove_instance_variable(:@name) if name.nil?
614
545
  end
615
546
 
616
547
  def database_server
617
- @database_server ||= DatabaseServer.find(self.database_server_id)
548
+ @database_server ||= DatabaseServer.find(database_server_id)
618
549
  end
619
550
 
620
551
  def database_server=(database_server)
@@ -635,23 +566,25 @@ module Switchman
635
566
  Shard.default
636
567
  end
637
568
 
638
- def activate(*categories)
639
- shards = hashify_categories(categories)
640
- Shard.activate(shards) do
641
- yield
642
- end
569
+ def original_id
570
+ id
571
+ end
572
+
573
+ def activate(*classes, &block)
574
+ shards = hashify_classes(classes)
575
+ Shard.activate(shards, &block)
643
576
  end
644
577
 
645
578
  # for use from console ONLY
646
- def activate!(*categories)
647
- shards = hashify_categories(categories)
579
+ def activate!(*classes)
580
+ shards = hashify_classes(classes)
648
581
  Shard.activate!(shards)
649
582
  nil
650
583
  end
651
584
 
652
585
  # custom serialization, since shard is self-referential
653
- def _dump(depth)
654
- self.id.to_s
586
+ def _dump(_depth)
587
+ id.to_s
655
588
  end
656
589
 
657
590
  def self._load(str)
@@ -659,45 +592,45 @@ module Switchman
659
592
  end
660
593
 
661
594
  def drop_database
662
- raise("Cannot drop the database of the default shard") if self.default?
595
+ raise('Cannot drop the database of the default shard') if default?
663
596
  return unless read_attribute(:name)
664
597
 
665
598
  begin
666
- adapter = self.database_server.config[:adapter]
599
+ adapter = database_server.config[:adapter]
667
600
  sharding_config = Switchman.config || {}
668
601
  drop_statement = sharding_config[adapter]&.[](:drop_statement)
669
602
  drop_statement ||= sharding_config[:drop_statement]
670
603
  if drop_statement
671
604
  drop_statement = Array(drop_statement).dup.
672
- map { |statement| statement.gsub('%{name}', self.name) }
605
+ map { |statement| statement.gsub('%{name}', name) }
673
606
  end
674
607
 
675
608
  case adapter
676
- when 'mysql', 'mysql2'
677
- self.activate do
678
- ::GuardRail.activate(:deploy) do
679
- drop_statement ||= "DROP DATABASE #{self.name}"
680
- Array(drop_statement).each do |stmt|
681
- ::ActiveRecord::Base.connection.execute(stmt)
682
- end
609
+ when 'mysql', 'mysql2'
610
+ activate do
611
+ ::GuardRail.activate(:deploy) do
612
+ drop_statement ||= "DROP DATABASE #{name}"
613
+ Array(drop_statement).each do |stmt|
614
+ ::ActiveRecord::Base.connection.execute(stmt)
683
615
  end
684
616
  end
685
- when 'postgresql'
686
- self.activate do
687
- ::GuardRail.activate(:deploy) do
688
- # Shut up, Postgres!
689
- conn = ::ActiveRecord::Base.connection
690
- old_proc = conn.raw_connection.set_notice_processor {}
691
- begin
692
- drop_statement ||= "DROP SCHEMA #{self.name} CASCADE"
693
- Array(drop_statement).each do |stmt|
694
- ::ActiveRecord::Base.connection.execute(stmt)
695
- end
696
- ensure
697
- conn.raw_connection.set_notice_processor(&old_proc) if old_proc
617
+ end
618
+ when 'postgresql'
619
+ activate do
620
+ ::GuardRail.activate(:deploy) do
621
+ # Shut up, Postgres!
622
+ conn = ::ActiveRecord::Base.connection
623
+ old_proc = conn.raw_connection.set_notice_processor {}
624
+ begin
625
+ drop_statement ||= "DROP SCHEMA #{name} CASCADE"
626
+ Array(drop_statement).each do |stmt|
627
+ ::ActiveRecord::Base.connection.execute(stmt)
698
628
  end
629
+ ensure
630
+ conn.raw_connection.set_notice_processor(&old_proc) if old_proc
699
631
  end
700
632
  end
633
+ end
701
634
  end
702
635
  rescue
703
636
  logger.info "Drop failed: #{$!}"
@@ -707,8 +640,9 @@ module Switchman
707
640
  # takes an id local to this shard, and returns a global id
708
641
  def global_id_for(local_id)
709
642
  return nil unless local_id
643
+
710
644
  self.class.signed_id_operation(local_id) do |abs_id|
711
- abs_id + self.id * IDS_PER_SHARD
645
+ abs_id + (id * IDS_PER_SHARD)
712
646
  end
713
647
  end
714
648
 
@@ -718,7 +652,8 @@ module Switchman
718
652
  end
719
653
 
720
654
  def destroy
721
- raise("Cannot destroy the default shard") if self.default?
655
+ raise('Cannot destroy the default shard') if default?
656
+
722
657
  super
723
658
  end
724
659
 
@@ -727,7 +662,7 @@ module Switchman
727
662
  def clear_cache
728
663
  Shard.default.activate do
729
664
  Switchman.cache.delete(['shard', id].join('/'))
730
- Switchman.cache.delete("default_shard") if default?
665
+ Switchman.cache.delete('default_shard') if default?
731
666
  end
732
667
  self.class.clear_cache
733
668
  end
@@ -736,22 +671,14 @@ module Switchman
736
671
  database_server.shard_name(self)
737
672
  end
738
673
 
739
- def on_rollback
740
- # make sure all connection pool proxies are referencing valid pools
741
- ::ActiveRecord::Base.connection_handler.connection_pools.each do |pool|
742
- next unless pool.is_a?(ConnectionPoolProxy)
743
-
744
- pool.remove_shard!(self)
745
- end
746
- end
747
-
748
- def hashify_categories(categories)
749
- if categories.empty?
750
- { :primary => self }
674
+ def hashify_classes(classes)
675
+ if classes.empty?
676
+ { ::ActiveRecord::Base => self }
751
677
  else
752
- categories.inject({}) { |h, category| h[category] = self; h }
678
+ classes.each_with_object({}) do |klass, h|
679
+ h[klass] = self
680
+ end
753
681
  end
754
682
  end
755
-
756
683
  end
757
684
  end